commit 28916eaf7dba7c50ec490fff11d0aa71a27d94ae Author: vetusmagnus <111835005+vetusmagnus@users.noreply.github.com> Date: Tue Oct 13 20:04:31 2026 -0400 V1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fade021 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# build output and downloads +dist/ +games/*/build/ +games/*/decomp/ +games/*/zig-cache/ +games/*/build_log.txt +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..4434e5a --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Project Solana + +Native PC versions of the PlayStation 2 Ratchet & Clank games, built on your own PC from your own disc. + +Project Solana is a launcher and a set of static recompilations. Each game's code is translated ahead of time from the game's executable into C, with the decompiled source from OpenRAC where it exists, and compiled into a native Windows program that reads everything else (models, textures, sound, movies) from your disc image at run time. + +**Nothing from the games is distributed here.** The launcher downloads only tools and source code; the game is built on your machine from a disc image you provide. + +## Playing + +1. Download `Project Solana.exe` from the [latest launcher release](../../releases) and put it in a folder of its own (it keeps its files next to itself). +2. Run it and add your disc image: **Add disc image**, **Scan a folder**, or drag an `.iso` onto the window. +3. Select the game and press **Build**. The first build downloads about 95 MB (Python and the zig compiler, then the game's build kit) and builds the game from your disc: 10 to 30 minutes depending on your CPU. It only happens once per game version. +4. Press **Play**. + +### Supported games + +| Game | Region | Disc executable | Status | +|---|---|---|---| +| Ratchet & Clank | PAL (v2.00) | `SCES_509.16`, MD5 `d86429e2eca4e1da7d7f1cc656af2574` | Playable start to finish | +| Ratchet & Clank | NTSC-U, NTSC-J | `SCUS_971.99`, `SCPS_150.37` | Recognised, not supported yet | +| Going Commando, Up Your Arsenal, Deadlocked | all | | Recognised, not supported yet | + +### Requirements + +- Windows 10 or 11, 64-bit +- A GPU with OpenGL 3.3 (Upscaled at 3 to 4x wants a GTX 1060 class card or better) +- A controller: Xbox, DualShock 4 or DualSense, over USB or Bluetooth +- About 2 GB free while building, 300 MB after + +### Settings + +Resolution (Native, or Upscaled to a whole multiple of the PS2's 448 lines that covers your screen), VSync, the graphics threads, performance statistics in the log, the length of the F9 glitch capture, and a level select for testing. **Rebuild the game** fetches the latest build kit and builds again; saves are kept. + +### Reporting problems + +- **Open log** shows `rac1_log.txt` (or the build's `build_log.txt` when a build failed). +- **F9** in the game records the next few seconds of graphics input to `gsrec_NN.bin` (about 260 MB per second of capture); attach it to a report together with what looked wrong. + +## Repository layout + +| Path | What | +|---|---| +| `launcher/` | `Project Solana.exe`: Win32 + GDI, no dependencies. `python launcher/build_launcher.py` builds it (needs zig: `pip install ziglang`) | +| `games/rac1/` | Ratchet & Clank's build kit: `build.py`, the recompilers (`tools/`), the host engine (`host/`: GS in OpenGL and software, DMA, VU, SPU2 and 989snd, disc, pad, memory card, movies), the wrapper (`launcher/`) | +| `games/rac1/dev/` | developer tools (GS capture replay, VU reference check); not part of the kit | +| `release/make_release.py` | makes a game's release files | +| `THIRD_PARTY_NOTICES.md` | pl_mpeg (MIT), jak-project's 989snd tables (ISC) | + +## How the Build button works + +The launcher fetches from the release named after the game (`releases/download/rac1/`): + +| File | What | +|---|---| +| `rac1.json` | version, file names, sizes and SHA-256s, the levels to build | +| `toolchain-win64.zip` | Python 3.12 (python-build-standalone), rabbitizer, zig 0.16; shared by all games, kept in `toolchain\` and fetched again only when its version changes | +| `rac1-build.zip` | `games/rac1` plus the decomp's source as `decomp/` (no `baserom/`) | + +It checks both zips against their SHA-256s, unpacks them next to itself (`toolchain\`, `games\rac1\`), runs `build.py --platform windows --iso `, and then deletes the intermediate files (about 1.3 GB) and the bytes it read from the disc, keeping `games\rac1\build\windows` (the game, its saves and logs). + +`solana.ini` `[build]` `release_url=` (a URL or a local folder laid out like the release) and `levels=` override where it fetches from and which levels it builds, for testing. + +## Making a release + +``` +python release/make_release.py rac1 --decomp ../rac1-decomp --version 2026.10.13 +gh release create rac1 dist/rac1/rac1.json dist/rac1/rac1-build.zip dist/rac1/toolchain-win64.zip +``` + +To update an existing game release, upload the new files over the old ones (`gh release upload rac1 dist/rac1/* --clobber`). Players get them with **Rebuild the game**. + +The launcher itself goes in a release of its own (for example `launcher-v1`) with `Project Solana.exe`. + +## Licence + +TODO (maintainers): choose a licence for this repository. Third-party code keeps its own (see `THIRD_PARTY_NOTICES.md`). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..8d95c00 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,60 @@ +# Third-party code + +## pl_mpeg + +`host/src/movie/pl_mpeg2.h` is pl_mpeg by Dominic Szablewski +(https://github.com/phoboslab/pl_mpeg), extended here for MPEG-2. It is under the MIT licence below. + +The MPEG-2 coefficient table B.15 and the run/level order in that file are +data from ISO/IEC 13818-2, cross-checked against FFmpeg's +`libavcodec/mpeg12data.c`. + +### pl_mpeg licence (MIT) + +Copyright (c) Dominic Szablewski - https://phoboslab.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## jak-project's 989snd (ISC) + +`host/src/rac1/snd989.c` and `host/src/rac1/snd989_tables.h` (the host's +989snd IOP driver) follow the jak-project's reimplementation of 989snd +(https://github.com/open-goal/jak-project, `game/sound/989snd` and +`game/sound/common`, by Ziemas) for the sound-effect grain semantics, the LFOs, +the volume and pan rules (MakeVolume, AdjustVolToGroup), the note-to-pitch +conversion, and they take from it the note pitch table, the pan table and the +SPU's Gaussian interpolation table. Those files are under the ISC licence: + +Copyright (c) 2021-2024 Ziemas + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The SPU2 reverb in the same file is written from the hardware's formula as +documented in nocash's psx-spx; its preset register values are the PlayStation +sound library's effect modes. diff --git a/games/rac1/README.md b/games/rac1/README.md new file mode 100644 index 0000000..0e53318 --- /dev/null +++ b/games/rac1/README.md @@ -0,0 +1,26 @@ +# Ratchet & Clank (PAL v2.00): build kit + +A static recompilation of Ratchet & Clank (`SCES_509.16`) for Windows (and Linux, for development). + +`build.py` reads the game's executable and level programs from your disc image, generates C from them (the decomp's own C where it has it, everything else translated instruction by instruction by `tools/recomp.py`, the VU microcode by `tools/vu_recomp.py`), and compiles it with the host engine in `host/` into `build/windows/rac1_game.dll` and `rac1_wrapper.exe`. + +``` +python build.py --platform windows --iso "Ratchet & Clank (Europe).iso" --repo ../rac1-decomp --levels 0,1,...,18 +``` + +Needs Python 3, `pip install rabbitizer ziglang`. The Project Solana launcher runs this for players with its own copies of both. + +| Path | What | +|---|---| +| `tools/gen_host.py` | the decomp's C made host-ready (types, calls between C and translated code) | +| `tools/recomp.py` | MIPS (R5900) to C, the executable and each level | +| `tools/vu_recomp.py` | VU0/VU1 microprograms to C | +| `tools/level_dump.py` | a level's program from the disc image | +| `host/src/gs/` | the Graphics Synthesizer: software and OpenGL 3.3 (upscaling: `UYA_SCALE`) | +| `host/src/gfx/` | VIF, GIF and the VU runtime | +| `host/src/sdk/`, `host/src/rac1/` | Sony library entry points, 989snd and the SPU2, levels, boot | +| `host/overrides/` | hand-ported assembly functions | +| `launcher/` | `rac1_wrapper.exe`: reserves PS2 RAM at its real addresses, joins split DLLs, runs the DLL | +| `docs/LEVELS.md` | how level programs are built in | + +Environment variables for testing (`UYA_FPSLOG`, `UYA_GSREC`, `UYA_LEVEL`, `UYA_SCALE`, ...) are described where they are read. diff --git a/games/rac1/build.py b/games/rac1/build.py new file mode 100644 index 0000000..3128109 --- /dev/null +++ b/games/rac1/build.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""build.py: generate host sources from OpenRAC rac1-decomp and build the RAC1 Wrapper. + +usage: python build.py [--repo PATH_TO_DECOMP] [--platform linux|windows] [-j N] + [--iso PATH_TO_DISC_IMAGE] [--levels 00,01,...|none] + +Needs Python 3 and zig (pip install ziglang, or zig on PATH). The decomp repo +is only read. Output: build//rac1_wrapper[.exe] + +Level programs (docs/LEVELS.md): each level listed in --levels (default 00, +Veldin 1) is compiled from src/overlays and its own bytes. Those bytes come +from your disc image (--iso, else RAC1_ISO, else iso= in +build//rac1_wrapper.cfg), or from an OpenRAC dump in the decomp +repo (baserom/overlays/level_NN); they are extracted to build/levels/ and +never leave your machine. +""" +import argparse, glob, json, os, shutil, subprocess, sys +from concurrent.futures import ThreadPoolExecutor, as_completed + +HERE = os.path.dirname(os.path.abspath(__file__)) +ap = argparse.ArgumentParser() +ap.add_argument("--repo", default=os.environ.get("UYA_DECOMP", os.path.join(HERE, "..", "rac1-decomp-main"))) +ap.add_argument("--platform", default="windows" if os.name == "nt" else "linux") +ap.add_argument("-j", type=int, default=os.cpu_count() or 2) +ap.add_argument("-O", default="2") +ap.add_argument("--iso", default=None) +ap.add_argument("--levels", default="00") +a = ap.parse_args() + +zig = [shutil.which("zig")] if shutil.which("zig") else [sys.executable, "-m", "ziglang"] +target = {"linux": "x86-linux-musl", "windows": "x86-windows-gnu"}[a.platform] +out = os.path.join(HERE, "build", a.platform) +gen = os.path.join(out, "gen") +stage = os.path.join(out, "gen.new") +obj = os.path.join(out, "obj") +os.makedirs(obj, exist_ok=True) +levels = [] if a.levels in ("", "none") else sorted({int(x) for x in a.levels.split(",")}) + + +def find_iso(): + if a.iso: return a.iso + if os.environ.get("RAC1_ISO"): return os.environ["RAC1_ISO"] + cfg = os.path.join(out, "rac1_wrapper.cfg") + if os.path.exists(cfg): + for l in open(cfg, errors="replace"): + if l.strip().startswith("iso="): return l.strip()[4:].strip() + return None + + +def sync(src, dst): + """copy the freshly generated tree over gen/, touching only files that + changed, so unchanged sources keep their objects""" + os.makedirs(dst, exist_ok=True) + keep = set() + for root, _, fs in os.walk(src): + for f in fs: + s = os.path.join(root, f) + rel = os.path.relpath(s, src) + d = os.path.join(dst, rel) + keep.add(rel) + os.makedirs(os.path.dirname(d), exist_ok=True) + if not os.path.exists(d) or open(s, "rb").read() != open(d, "rb").read(): + shutil.copyfile(s, d) + for root, _, fs in os.walk(dst): + for f in fs: + rel = os.path.relpath(os.path.join(root, f), dst) + if rel not in keep: os.remove(os.path.join(root, f)) + shutil.rmtree(src) + + +def iso_file(iso, name): + """a file in the disc image's root directory (ISO 9660)""" + import struct + with open(iso, "rb") as f: + f.seek(16 * 2048); pvd = f.read(2048) + if pvd[1:6] != b"CD001": return None + dlsn, dlen = struct.unpack_from(" os.path.getmtime(src) and t > headers_time and (not hdr or t > os.path.getmtime(hdr)): + return o, None + r = subprocess.run(zig + ["cc"] + common + extra + ["-c", src, "-o", o], capture_output=True, text=True) + return o, (r.stderr if r.returncode else None) + +progress("compile", 0, len(jobs)) +with ThreadPoolExecutor(a.j) as ex: + futs = [ex.submit(cc, j) for j in jobs] + done_n = 0 + for _ in as_completed(futs): + done_n += 1 + if done_n % 8 == 0 or done_n == len(jobs): progress("compile", done_n, len(jobs)) + res = [f.result() for f in futs] +progress("link", 0, 1) + + +def objs_rsp(name): + """the object files as a response file (@file): Windows limits a command line to + 32767 characters, and the ~5700 objects of a full build are far more than that""" + p = os.path.join(obj, name + ".rsp") + with open(p, "w") as f: + for o, _ in res: f.write('"%s"\n' % o.replace("\\", "/")) + return "@" + p.replace("\\", "/") +errs = [e for _, e in res if e] +for e in errs: print(e[-3000:]) +if errs: + sys.exit("%d files failed to compile" % len(errs)) +if a.platform == "linux": + exe = os.path.join(out, "rac1_wrapper") + link = zig + ["cc", "-target", target, "-o", exe, objs_rsp("link")] + ["-static", "-no-pie", "-Wl,--image-base=0x40000000"] + r = subprocess.run(link, capture_output=True, text=True) + if r.returncode: + print(r.stderr[-6000:]); sys.exit("link failed") +else: + # the menu and runtime as a DLL at 0x40000000, plus the small launcher that owns PS2 RAM + dll = os.path.join(out, "rac1_game.dll") + link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32"] + r = subprocess.run(link, capture_output=True, text=True) + if r.returncode: + print(r.stderr[-6000:]); sys.exit("link failed (dll)") + exe = os.path.join(out, "rac1_wrapper.exe") + L = os.path.join(HERE, "launcher") + link = zig + ["cc", "-target", target, "-O2", "-nostdlib", "-o", exe, os.path.join(L, "launcher_win32.c"), + os.path.join(L, "ps2ram_win32.c"), "-lkernel32", "-luser32", + "-Wl,--image-base=0x10000", "-Wl,--entry=launcher_start@0", "-Wl,--no-dynamicbase"] + r = subprocess.run(link, capture_output=True, text=True) + if r.returncode: + print(r.stderr[-6000:]); sys.exit("link failed (launcher)") + # the library app (frontend/solana.c): build/Project Solana.exe, which finds the game in build/windows + F = os.path.join(HERE, "frontend") + if not os.path.isdir(F): F = None # (the game kit players download has no launcher source) + link = None if not F else zig + ["cc", "-target", target, "-O2", "-municode", "-Wl,--subsystem,windows", "-o", os.path.join(HERE, "build", "Project Solana.exe"), + os.path.join(F, "solana.c"), os.path.join(F, "solana.rc"), "-lgdi32", "-luser32", "-lshell32", "-lcomdlg32", "-lole32", "-lmsimg32", "-lwinhttp", "-lbcrypt", "-lwindowscodecs", "-luuid"] + r = subprocess.run(link, capture_output=True, text=True, cwd=F) if link else None + if r and r.returncode: + print(r.stderr[-6000:]); sys.exit("link failed (Project Solana.exe)") +progress("done", 1, 1) +print("built", exe) diff --git a/games/rac1/dev/gs_gl_egl.c b/games/rac1/dev/gs_gl_egl.c new file mode 100644 index 0000000..5891d84 --- /dev/null +++ b/games/rac1/dev/gs_gl_egl.c @@ -0,0 +1,54 @@ +/* gs_gl_egl.c: an OpenGL 3.3 core context without a window for the GL + * renderer's test harness (tools/gs_replay.c built with GS_GL=1, 64-bit): + * EGL's surfaceless platform (Mesa, e.g. llvmpipe), loaded with dlopen so no + * GL or EGL headers or link libraries are needed. */ +#include +#include +#include +#include + +typedef void *EGLDisplay, *EGLContext, *EGLConfig, *EGLSurface; +typedef int32_t EGLint; +typedef unsigned EGLBoolean, EGLenum; +static void *egl; +static void *(*eglGetProcAddress_)(const char *); +void *(*gl_proc_override)(const char *); /* the harness's stubs (REPLAY_GLNULL) */ +void *plat_gl_proc(const char *name) +{ + if (gl_proc_override) return gl_proc_override(name); + if (!egl) return 0; + void *p = eglGetProcAddress_ ? eglGetProcAddress_(name) : 0; + if (!p && egl) p = dlsym(egl, name); + return p; +} + +static EGLBoolean (*egl_mc)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); +static EGLDisplay egl_d; static EGLContext egl_c; +int egl_gl_init(void) +{ + egl = dlopen("libEGL.so.1", RTLD_NOW); + if (!egl) { fprintf(stderr, "no libEGL: %s\n", dlerror()); return 0; } + eglGetProcAddress_ = (void *(*)(const char *))dlsym(egl, "eglGetProcAddress"); + EGLDisplay (*getPlatformDisplay)(EGLenum, void *, const EGLint *) = (EGLDisplay (*)(EGLenum, void *, const EGLint *))eglGetProcAddress_("eglGetPlatformDisplayEXT"); + EGLBoolean (*initialize)(EGLDisplay, EGLint *, EGLint *) = (EGLBoolean (*)(EGLDisplay, EGLint *, EGLint *))dlsym(egl, "eglInitialize"); + EGLBoolean (*bindAPI)(EGLenum) = (EGLBoolean (*)(EGLenum))dlsym(egl, "eglBindAPI"); + EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *) = (EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *))dlsym(egl, "eglChooseConfig"); + EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *) = (EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *))dlsym(egl, "eglCreateContext"); + EGLBoolean (*makeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext) = (EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))dlsym(egl, "eglMakeCurrent"); + if (!getPlatformDisplay || !initialize || !bindAPI || !chooseConfig || !createContext || !makeCurrent) { fprintf(stderr, "EGL entry points missing\n"); return 0; } + EGLDisplay d = getPlatformDisplay(0x31DD /* EGL_PLATFORM_SURFACELESS_MESA */, 0, 0); + EGLint ma, mi; + if (!d || !initialize(d, &ma, &mi)) { fprintf(stderr, "eglInitialize failed\n"); return 0; } + bindAPI(0x30A2 /* EGL_OPENGL_API */); + EGLint ca[] = { 0x3040 /* RENDERABLE_TYPE */, 0x0008 /* OPENGL_BIT */, 0x3038 }; + EGLConfig cfg; EGLint n = 0; + if (!chooseConfig(d, ca, &cfg, 1, &n) || n < 1) cfg = 0; /* EGL_KHR_no_config_context */ + EGLint xa[] = { 0x3098, 3, 0x30FB, 3, 0x30FD, 1 /* core */, 0x3038 }; + EGLContext c = createContext(d, cfg, 0, xa); + if (!c) { fprintf(stderr, "eglCreateContext failed\n"); return 0; } + if (!makeCurrent(d, 0, 0, c)) { fprintf(stderr, "eglMakeCurrent failed\n"); return 0; } + egl_mc = makeCurrent; egl_d = d; egl_c = c; + return 1; +} +/* the context current on the calling thread, or released (the GL thread takes it over) */ +void egl_gl_current(int on) { if (egl_mc && !egl_mc(egl_d, 0, 0, on ? egl_c : 0)) fprintf(stderr, "eglMakeCurrent(%d) failed\n", on); else if (getenv("REPLAY_GLDBG")) fprintf(stderr, "eglMakeCurrent(%d) ok\n", on); } diff --git a/games/rac1/dev/gs_replay.c b/games/rac1/dev/gs_replay.c new file mode 100644 index 0000000..9e3658b --- /dev/null +++ b/games/rac1/dev/gs_replay.c @@ -0,0 +1,205 @@ +/* gs_replay.c: runs GS input recorded with UYA_GSREC (host/src/gs/gs.c) + * through a build of gs.c and prints a hash of GS memory and of the shown + * frame after every vsync, and the time the GS took. Two builds of gs.c that + * print the same hashes drew the same pixels. + * + * build (32-bit like the game; GS_C is the gs.c to test): + * zig cc -target x86-linux-musl -O2 -march=nehalem -mfpmath=sse -malign-double -fno-strict-aliasing -fwrapv \ + * -I host/include -I host/src/gs -DGS_C='"../host/src/gs/gs.c"' tools/gs_replay.c host/src/gs/gs_gl.c -o gs_replay -static -lpthread + * (a gs.c elsewhere: an absolute path in GS_C and -I host/src/gs) + * run: gs_replay rec.bin [frames_dir] (UYA_GS_THREADS=n: drawing threads, default 0; + * REPLAY_QUIET=1: no GS log; REPLAY_PROF=file: a program counter histogram, as UYA_PROF) + * + * The GL renderer (host/src/gs/gs_gl.c) in a 64-bit build with an EGL + * context without a window (Mesa llvmpipe), frames read back from the GPU: + * gcc -O2 -DGS_GL -I host/include -I host/src/gs -DGS_C='"../host/src/gs/gs.c"' tools/gs_replay.c \ + * host/src/gs/gs_gl.c tools/gs_gl_egl.c -o gs_replay_gl -ldl -lpthread -lm + * REPLAY_SOFT=1: the software GS in the same binary; REPLAY_GLNULL=1: GL calls do nothing (the + * CPU cost of the front end and the batching alone); REPLAY_VRAMSYNC=1: GPU targets read back into + * GS memory at each vsync, so the vram hash compares */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include GS_C + +/* REPLAY_PROF=file: a histogram of the program counter, 1 ms of CPU time apart ("eip tid count") */ +static u32 prof_eip[1 << 16], prof_n[1 << 16]; +static void on_prof(int sig, siginfo_t *si, void *ctxv) +{ + u32 eip = (u32)((ucontext_t *)ctxv)->uc_mcontext.gregs[ +#ifdef __x86_64__ + REG_RIP +#else + REG_EIP +#endif + ], h = (eip * 2654435761u) >> 16; + (void)sig; (void)si; + for (int k = 0; k < 64; k++) { u32 j = (h + k) & 0xFFFF; if (prof_n[j] && prof_eip[j] != eip) continue; prof_eip[j] = eip; prof_n[j]++; return; } +} +static void prof_dump(void) +{ + FILE *f = fopen(getenv("REPLAY_PROF"), "w"); + if (f) { for (int j = 0; j < 1 << 16; j++) if (prof_n[j]) fprintf(f, "%08x 0 %u\n", prof_eip[j], prof_n[j]); fclose(f); } +} + +#ifdef GS_GL +#include "gs_gl_api.h" +int egl_gl_init(void); +int plat_gl_active; +static int gl_null; +int plat_gl_init(void) { return gl_null ? 1 : egl_gl_init(); } +void egl_gl_current(int on); +void plat_gl_current(int on) { if (!gl_null) egl_gl_current(on); } +void plat_gl_swap(void) {} +void plat_gl_size(int *w, int *h) { *w = 640; *h = 480; } +/* REPLAY_GLNULL: every GL call returns 0 (a few return what initialisation needs) */ +static long gl_nop(void) { return 0; } +static void GLAPIENTRY gl_getiv(GLuint a, GLenum b, GLint *v) { (void)a; (void)b; *v = 1; } +static char gl_mapbuf[8 << 20]; +static void *GLAPIENTRY gl_map(GLenum a, GLintptr b, GLsizeiptr c, GLbitfield d) { (void)a; (void)b; (void)c; (void)d; return gl_mapbuf; } +static GLuint gl_names; +static void GLAPIENTRY gl_gen(GLsizei n, GLuint *o) { for (int i = 0; i < n; i++) o[i] = ++gl_names; } +static GLuint GLAPIENTRY gl_create(void) { return ++gl_names; } +static GLuint GLAPIENTRY gl_create1(GLenum e) { (void)e; return ++gl_names; } +void *plat_gl_proc(const char *name); +static void *null_proc(const char *n) +{ + if (!strcmp(n, "glGetShaderiv") || !strcmp(n, "glGetProgramiv")) return (void *)gl_getiv; + if (!strcmp(n, "glMapBufferRange")) return (void *)gl_map; + if (!strcmp(n, "glGenTextures") || !strcmp(n, "glGenBuffers") || !strcmp(n, "glGenVertexArrays") || !strcmp(n, "glGenFramebuffers")) return (void *)gl_gen; + if (!strcmp(n, "glCreateProgram")) return (void *)gl_create; + if (!strcmp(n, "glCreateShader")) return (void *)gl_create1; + return (void *)gl_nop; +} +#else +int plat_gl_active; /* (no GL in this build) */ +int plat_gl_init(void) { return 0; } +void plat_gl_current(int on) { (void)on; } +void plat_gl_swap(void) {} +void plat_gl_size(int *w, int *h) { *w = 640; *h = 480; } +void *plat_gl_proc(const char *name) { (void)name; return 0; } +#endif +static uint32_t replay_vs; +uint32_t uya_frame_count(void) { return replay_vs; } +u32 vu_nonfinite; +const char *plat_exe_dir(void) { return "."; } +void uya_log(const char *fmt, ...) +{ + if (getenv("REPLAY_QUIET")) return; + va_list ap; va_start(ap, fmt); fprintf(stderr, "[gs] "); vfprintf(stderr, fmt, ap); fputc('\n', stderr); va_end(ap); +} +int plat_present(const uint32_t *px, int w, int h) { (void)px; (void)w; (void)h; return 1; } +unsigned long long nsends, uya_t_vu; +void uya_vu_dump_kick(void) {} +void uya_vif1_dump_hist(void) {} + +static uint32_t hash32(const void *p, size_t n) /* FNV-1a over words */ +{ + const uint32_t *w = p; uint32_t h = 2166136261u; + for (size_t i = 0; i < n / 4; i++) h = (h ^ w[i]) * 16777619u; + return h; +} +static double now_s(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec / 1e9; } +static double cpu_s(void) { struct timespec t; clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t); return t.tv_sec + t.tv_nsec / 1e9; } +static void rd(FILE *f, void *p, size_t n) { if (fread(p, 1, n, f) != n) { fprintf(stderr, "short read\n"); exit(1); } } + +int main(int argc, char **argv) +{ + if (argc < 2) { fprintf(stderr, "usage: gs_replay rec.bin [frames_dir]\n"); return 1; } + if (!getenv("UYA_GS_THREADS")) setenv("UYA_GS_THREADS", "0", 1); + if (getenv("REPLAY_PROF")) { + struct sigaction sa; memset(&sa, 0, sizeof sa); sa.sa_flags = SA_SIGINFO | SA_RESTART; sa.sa_sigaction = on_prof; sigaction(SIGPROF, &sa, 0); + struct itimerval it = { { 0, 1000 }, { 0, 1000 } }; setitimer(ITIMER_PROF, &it, 0); atexit(prof_dump); + } + setenv("UYA_MTGS", "0", 1); +#ifdef GS_GL + if (!getenv("REPLAY_SOFT")) setenv("UYA_RENDERER", "gl", 1); + gl_null = getenv("REPLAY_GLNULL") != 0; + if (gl_null) { extern void *(*gl_proc_override)(const char *); gl_proc_override = null_proc; } /* the loader hands out the stubs */ +#endif + FILE *f = fopen(argv[1], "rb"); + if (!f) { perror(argv[1]); return 1; } + uya_gs_init(); + u32 m; rd(f, &m, 4); + if (m != 0x52534755u) { fprintf(stderr, "not a GS recording\n"); return 1; } +#define X(fl) { u32 n_; rd(f, &n_, 4); if (n_ != sizeof fl) { fprintf(stderr, "field " #fl " size %u, expected %u\n", n_, (u32)sizeof fl); return 1; } rd(f, &fl, sizeof fl); } + GS_SNAP_FIELDS(X) +#undef X + rd(f, vram, 4 << 20); + static u32 buf[(0x10000 + 16) * 4]; + static u32 *big; static size_t bign; + double t = 0, t0 = now_s(), c = 0; + u32 k; + while (fread(&k, 4, 1, f) == 1) { + if (k == 1) { + u32 h[2]; rd(f, h, 8); + u32 *d = buf; + if (h[1] > 0x10000) { if (bign < h[1] * 16) { free(big); big = malloc(h[1] * 16); bign = h[1] * 16; } d = big; } + rd(f, d, h[1] * 16); + double a = now_s(), ac = cpu_s(); + uya_gs_src = path_name[h[0] < 3 ? h[0] : 2]; + uya_gs_gif(d, h[1]); + t += now_s() - a; c += cpu_s() - ac; + } else if (k == 2) { + u64 r[6]; rd(f, r, 48); + double a = now_s(), ac = cpu_s(); + u32 w = 0, hh = 0; + if (getenv("REPLAY_DISPFBP")) r[2] = (r[2] & ~0x1FFull) | ((u64)strtol(getenv("REPLAY_DISPFBP"), 0, 16) / 32); /* testing: show another buffer */ + gs_drain(); + int ok; +#ifdef GS_GL + if (gsgl_on) { + gs_disp_t dd; + ok = gs_disp(r[0], r[1], r[2], r[3], r[4], r[5], &dd); + if (ok && !gl_null) ok = gsgl_capture(&dd, frame_px); + else gsgl_flush(); + w = dd.w; hh = dd.h; + if (getenv("REPLAY_VRAMSYNC")) gsgl_cpu_read(0, 0x4000); + if (ok && !gl_null && getenv("REPLAY_FULL") && argc > 2) { /* the upscaled picture too: full_NNNNN.ppm */ + int gsgl_capture_full(const gs_disp_t *d, u32 *out, int *ow, int *oh); + static u32 *big; int fw, fh; + if (!big) big = malloc((size_t)4096 * 4096 * 4); + if (gsgl_capture_full(&dd, big, &fw, &fh)) { + char p[512]; snprintf(p, sizeof p, "%s/full_%05u.ppm", argv[2], replay_vs + 1); + FILE *o = fopen(p, "wb"); + if (o) { fprintf(o, "P6 %d %d 255\n", fw, fh); + for (int y = 0; y < fh; y++) for (int x = 0; x < fw; x++) { u32 c = big[(size_t)y * fw + x]; fputc(c & 255, o); fputc((c >> 8) & 255, o); fputc((c >> 16) & 255, o); } + fclose(o); } + } + } + } else +#endif + ok = gs_capture(r[0], r[1], r[2], r[3], r[4], r[5], frame_px, &w, &hh); + t += now_s() - a; c += cpu_s() - ac; + replay_vs++; + printf("vsync %u vram %08X frame %08X\n", replay_vs, hash32(vram, 4 << 20), ok ? hash32(frame_px, (size_t)w * hh * 4) : 0); + if (argc > 2 && ok) { + char p[512]; snprintf(p, sizeof p, "%s/r_%05u.ppm", argv[2], replay_vs); + FILE *o = fopen(p, "wb"); + if (o) { fprintf(o, "P6 %u %u 255\n", w, hh); + for (u32 i = 0; i < w * hh; i++) { u32 c = frame_px[i]; fputc((c >> 16) & 255, o); fputc((c >> 8) & 255, o); fputc(c & 255, o); } + fclose(o); } + } + } else if (k == 3) { + u32 h[4]; rd(f, h, 16); + u32 *px = malloc((size_t)h[2] * h[3] * 4); rd(f, px, (size_t)h[2] * h[3] * 4); + double a = now_s(); + uya_gs_write_ct32(h[0], h[1], (int)h[2], (int)h[3], px); + t += now_s() - a; + free(px); + } else { fprintf(stderr, "bad record %u\n", k); return 1; } + } +#ifdef GS_GL + if (gsgl_on) gsgl_stats((int)replay_vs); +#endif + fprintf(stderr, "%u vsyncs: GS time %.3f s (%.2f ms/vsync), CPU %.2f ms/vsync, wall %.3f s\n", replay_vs, t, replay_vs ? 1000 * t / replay_vs : 0, + replay_vs ? 1000 * c / replay_vs : 0, now_s() - t0); + return 0; +} diff --git a/games/rac1/dev/vu_refcheck.py b/games/rac1/dev/vu_refcheck.py new file mode 100644 index 0000000..e764230 --- /dev/null +++ b/games/rac1/dev/vu_refcheck.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +"""vu_refcheck.py DECOMP_REPO IMAGE VU [vucap_before.bin vucap_after.bin] + +Testing aid for tools/vu_recomp.py: a plain interpreter of VU micro code that +replays a state the running wrapper captured (UYA_VUCAPTURE=image,n, see +host/src/gfx/vu.c) and compares its result with what the translated C +produced: registers, flags, Q and data memory. It works out stalls at run +time (the translator does it statically), so it also checks that model. The +MAC/status flags are only compared for images that read them (the +translation skips them otherwise). Needs numpy. +""" +import os, struct, sys +import numpy as np +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from vu_isa import decode_pair, XYZW + +f32 = np.float32 +np.seterr(all="ignore") + +def load_images(): + d = open(os.path.join(sys.argv[1], "baserom", "SCES_509.16"), "rb").read() + e_shoff, = struct.unpack_from("= 4 + SZ + msz + 8 else (0, 0) + vf = np.frombuffer(st[0:512], dtype=np.uint32).reshape(32, 4).copy() + acc = np.frombuffer(st[512:528], dtype=np.uint32).copy() + q, i, p, r = struct.unpack_from("<4I", st, 528) + vi = list(struct.unpack_from("<16I", st, 544)) + clip, status, mac = struct.unpack_from("<3I", st, 608) + return dict(pc=pc, vf=vf, acc=acc, q=q, i=i, vi=vi, clip=clip, status=status, mac=mac, mem=mem, top=top, itop=itop) + +def fl(u): return np.array([u], dtype=np.uint32).view(np.float32)[0] +def bits(x): return int(np.array([x], dtype=np.float32).view(np.uint32)[0]) + +class VU: + def __init__(s, st, code, vu): + s.vf = st["vf"]; s.acc = st["acc"]; s.vi = st["vi"]; s.q = st["q"]; s.i = st["i"] + s.clip = st["clip"]; s.status = st["status"]; s.mac = st["mac"]; s.mem = st["mem"] + s.code = code; s.vu = vu; s.qmask = 0x3FF if vu else 0xFF + s.cyc = 0; s.ready = {} # (reg, comp) -> cycle + s.fpipe = [] # (rdy, mac, status) + s.cpipe = [] + s.cl_int = s.clip; s.st_int = s.status + s.qp = None # (rdy, val) + s.kicks = [] + s.top, s.itop = st["top"], st["itop"] + s.vhist = [] + s.targets = set() + for pc_, w_ in code.items(): + l_ = decode_pair(*w_)[1] + if l_ is not None and l_.name in ("b", "bal", "ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez"): + s.targets.add((pc_ + 8 + l_.imm * 8) & (0x3FF8 if vu else 0xFF8)) + def F(s, r, c): return fl(int(s.vf[r][c])) + def setF(s, r, c, x): + if r: s.vf[r][c] = bits(x) + def memw(s, qa, c): o = ((qa & s.qmask) << 4) + 4 * c; return struct.unpack_from(" maxsteps: raise Exception("too many steps") + lo, hi = s.code[pc] + u, l, flg, imm = decode_pair(lo, hi) + # stalls + reads = s.reads(u, l) + t = s.cyc + 1 + need = max([s.ready.get(x, 0) for x in reads] + [0]) + t = max(t, need) + if l is not None and l.name in ("waitq", "div", "sqrt", "rsqrt") and s.qp: + t = max(t, s.qp[0]) + stalled = t > s.cyc + 1 + s.cyc = t + s.flsync() + # upper + ures = s.upper(u) + # lower + nb = None + before_vi = list(s.vi) + if imm is not None: + s.i = imm + elif l is not None: + # branch VI delay (see tools/vu_recomp.py): the branch reads the value + # from before a run of writes to its register right before it + patch = {} + if l.name in ("ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez", "jr", "jalr") and not stalled and pc not in s.targets: + regs = [l.is_ & 15] + ([l.it & 15] if l.name in ("ibeq", "ibne") else []) + for x in regs: + if not x: continue + h = s.vhist + if not h or h[-1][0] != x or h[-1][2]: continue + k = len(h) - 1 + while k > 0 and len(h) - k < 4 and h[k - 1][0] == x and not h[k - 1][2] and not h[k][3] and not h[k][4]: + k -= 1 + patch[x] = h[k][1] + break + real = {x: s.vi[x] for x in patch} + for x, val in patch.items(): s.vi[x] = val + nb = s.lower(l, pc) + for x, val in patch.items(): + if s.vi[x] == val: s.vi[x] = real[x] + # history of VI writes: (reg, value before, reads flags, stalled, branch target) + if l is not None and imm is None: + wr = vi_written(l) + s.vhist.append((wr, before_vi[wr] if wr else 0, l.name in FLAGREAD, stalled, pc in s.targets)) + if len(s.vhist) > 8: s.vhist.pop(0) + else: + s.vhist.append((0, 0, False, stalled, pc in s.targets)) + if len(s.vhist) > 8: s.vhist.pop(0) + # commit upper + if ures: ures() + for x in s.writes(u, l): + s.ready[x] = s.cyc + 4 + # control + if ending: + return pc + 8 + if bpending is not None: + tgt = bpending; bpending = None + if flg["E"]: ending = 1 + pc = tgt + continue + if flg["E"]: + ending = 1 + if nb is not None: + bpending = nb + pc = pc + 8 + def reads(s, u, l): + r = [] + k = u.kind + if k == "arith": + for c in u.comps: + r.append((u.fs, c)) + if u.bc in ("x","y","z","w"): r.append((u.ft, XYZW.index(u.bc))) + elif u.bc is None: r.append((u.ft, c)) + elif k in ("opmula", "opmsub"): + for c in range(3): r += [(u.fs, c), (u.ft, c)] + elif k in ("itof", "ftoi", "abs"): r += [(u.fs, c) for c in u.comps] + elif k == "clip": r += [(u.fs, 0), (u.fs, 1), (u.fs, 2), (u.ft, 3)] + if l is not None: + n = l.name + if n in ("sq", "sqi", "sqd", "move"): r += [(l.fs, c) for c in l.comps] + elif n == "mr32": r += [(l.fs, (c + 1) & 3) for c in l.comps] + elif n == "mtir": r.append((l.fs, l.fsf)) + elif n == "div" or n == "rsqrt": r += [(l.fs, l.fsf), (l.ft, l.ftf)] + elif n == "sqrt": r.append((l.ft, l.ftf)) + return [x for x in r if x[0]] + def writes(s, u, l): + w = [] + k = u.kind + if k == "arith" and not u.acc: w += [(u.fd, c) for c in u.comps] + elif k == "opmsub": w += [(u.fd, c) for c in u.comps if c < 3] + elif k in ("itof", "ftoi", "abs"): w += [(u.ft, c) for c in u.comps] + if l is not None and l.name in ("lq", "lqi", "lqd", "move", "mr32", "mfir"): + w += [(l.ft, c) for c in l.comps] + return [x for x in w if x[0]] + def upper(s, u): + k = u.kind + if k == "nop": return None + if k == "arith": + base = u.base if u.acc else u.name + def B(c): + if u.bc in ("x","y","z","w"): return s.F(u.ft, XYZW.index(u.bc)) + if u.bc == "q": return fl(s.q) + if u.bc == "i": return fl(s.i) + return s.F(u.ft, c) + res = {} + for c in u.comps: + a, b = s.F(u.fs, c), B(c) + accv = fl(int(s.acc[c])) + if base == "add": x = f32(a + b) + elif base == "sub": x = f32(a - b) + elif base == "mul": x = f32(a * b) + elif base == "madd": x = f32(accv + f32(a * b)) + elif base == "msub": x = f32(accv - f32(a * b)) + elif base == "max": x = np.fmax(a, b) + elif base == "mini": x = np.fmin(a, b) + res[c] = x + def commit(): + for c, x in res.items(): + if u.acc: s.acc[c] = bits(x) + elif u.fd: s.vf[u.fd][c] = bits(x) + if base in ("add", "sub", "mul", "madd", "msub"): + s.push_flags(res) + return commit + if k in ("opmula", "opmsub"): + ex = {0: (1, 2), 1: (2, 0), 2: (0, 1)} + res = {} + for c in [c for c in u.comps if c < 3]: + a, b = ex[c] + pr = f32(s.F(u.fs, a) * s.F(u.ft, b)) + res[c] = pr if k == "opmula" else f32(fl(int(s.acc[c])) - pr) + def commit(): + for c, x in res.items(): + if k == "opmula": s.acc[c] = bits(x) + elif u.fd: s.vf[u.fd][c] = bits(x) + s.push_flags(res) + return commit + if k == "itof": + res = {c: bits(f32(np.int32(np.uint32(s.vf[u.fs][c]).view(np.int32)) / f32(1 << u.shift))) for c in u.comps} + def commit(): + for c, x in res.items(): + if u.ft: s.vf[u.ft][c] = x + return commit + if k == "ftoi": + res = {} + for c in u.comps: + x = float(s.F(u.fs, c)) * (1 << u.shift) + x = f32(x) + if x != x: v = 0 + elif x >= 2147483647.0: v = 0x7FFFFFFF + elif x <= -2147483648.0: v = -0x80000000 + else: v = int(x) + res[c] = v & 0xFFFFFFFF + def commit(): + for c, x in res.items(): + if u.ft: s.vf[u.ft][c] = x + return commit + if k == "abs": + res = {c: int(s.vf[u.fs][c]) & 0x7FFFFFFF for c in u.comps} + def commit(): + for c, x in res.items(): + if u.ft: s.vf[u.ft][c] = x + return commit + if k == "clip": + w = abs(s.F(u.ft, 3)); x, y, z = s.F(u.fs, 0), s.F(u.fs, 1), s.F(u.fs, 2) + f = (x > w) | ((x < -w) << 1) | ((y > w) << 2) | ((y < -w) << 3) | ((z > w) << 4) | ((z < -w) << 5) + def commit(): + s.cl_int = ((s.cl_int << 6) | int(f)) & 0xFFFFFF + s.cpipe.append((s.cyc + 4, s.cl_int)) + return commit + raise Exception("upper " + u.name) + def push_flags(s, res): + mac = 0 + for c, x in res.items(): + b = bits(x) + if (b & 0x7FFFFFFF) == 0: mac |= 8 >> c + if b >> 31: mac |= 0x80 >> c + st = (s.st_int & 0xFC0) | (1 if mac & 0xF else 0) | (2 if mac & 0xF0 else 0) + st |= (st & 3) << 6 + s.st_int = st + s.fpipe.append((s.cyc + 4, mac, st)) + def lower(s, l, pc): + n = l.name; vi = s.vi + V = lambda r: vi[r & 15] if r & 15 else 0 + if n == "move": + vals = [int(s.vf[l.fs][c]) for c in range(4)] + for c in l.comps: + if l.ft: s.vf[l.ft][c] = vals[c] + elif n == "mr32": + vals = [int(s.vf[l.fs][(c + 1) & 3]) for c in range(4)] + for c in l.comps: + if l.ft: s.vf[l.ft][c] = vals[c] + elif n in ("lq", "lqi", "lqd"): + if n == "lqd": s.setvi(l.is_, V(l.is_) - 1) + a = V(l.is_) + (l.imm if n == "lq" else 0) + for c in l.comps: + if l.ft: s.vf[l.ft][c] = s.memw(a, c) + if n == "lqi": s.setvi(l.is_, V(l.is_) + 1) + elif n in ("sq", "sqi", "sqd"): + if n == "sqd": s.setvi(l.it, V(l.it) - 1) + a = V(l.it) + (l.imm if n == "sq" else 0) + for c in l.comps: s.setmem(a, c, int(s.vf[l.fs][c])) + if n == "sqi": s.setvi(l.it, V(l.it) + 1) + elif n in ("ilw", "ilwr"): + a = V(l.is_) + (l.imm if n == "ilw" else 0) + s.setvi(l.it, s.memw(a, l.comps[0])) + elif n in ("isw", "iswr"): + a = V(l.is_) + (l.imm if n == "isw" else 0) + for c in l.comps: s.setmem(a, c, V(l.it)) + elif n == "iadd": s.setvi(l.id_, V(l.is_) + V(l.it)) + elif n == "isub": s.setvi(l.id_, V(l.is_) - V(l.it)) + elif n == "iand": s.setvi(l.id_, V(l.is_) & V(l.it)) + elif n == "ior": s.setvi(l.id_, V(l.is_) | V(l.it)) + elif n == "iaddi": s.setvi(l.it, V(l.is_) + l.imm) + elif n == "iaddiu": s.setvi(l.it, V(l.is_) + l.imm) + elif n == "isubiu": s.setvi(l.it, V(l.is_) - l.imm) + elif n == "mtir": s.setvi(l.it, int(s.vf[l.fs][l.fsf])) + elif n == "mfir": + v = V(l.is_); v = v - 0x10000 if v & 0x8000 else v + for c in l.comps: + if l.ft: s.vf[l.ft][c] = v & 0xFFFFFFFF + elif n in ("div", "sqrt", "rsqrt"): + if s.qp: s.q = s.qp[1]; s.qp = None + if n == "div": + a, b = s.F(l.fs, l.fsf), s.F(l.ft, l.ftf) + v = bits(f32(a / b)) if b != 0 else (0xFF7FFFFF if (bits(a) ^ bits(b)) & 0x80000000 else 0x7F7FFFFF) + s.qp = (s.cyc + 7, v) + elif n == "sqrt": + s.qp = (s.cyc + 7, bits(f32(np.sqrt(abs(s.F(l.ft, l.ftf)))))) + else: + a, b = s.F(l.fs, l.fsf), f32(np.sqrt(abs(s.F(l.ft, l.ftf)))) + v = bits(f32(a / b)) if b != 0 else (0xFF7FFFFF if bits(a) & 0x80000000 else 0x7F7FFFFF) + s.qp = (s.cyc + 13, v) + elif n == "waitq": pass + elif n == "fcand": s.setvi(1, 1 if (s.clip & l.imm) else 0) + elif n == "fcor": s.setvi(1, 1 if ((s.clip | l.imm) & 0xFFFFFF) == 0xFFFFFF else 0) + elif n == "fceq": s.setvi(1, 1 if (s.clip & 0xFFFFFF) == l.imm else 0) + elif n == "fcset": + s.cpipe = []; s.clip = s.cl_int = l.imm + elif n == "fcget": s.setvi(l.it, s.clip & 0xFFF) + elif n == "fsand": s.setvi(l.it, s.status & l.imm) + elif n == "fmand": s.setvi(l.it, V(l.is_) & s.mac) + elif n == "fmor": s.setvi(l.it, V(l.is_) | s.mac) + elif n == "xgkick": s.kicks.append(V(l.is_) & 0x3FF) + elif n == "xtop": s.setvi(l.it, s.top) + elif n == "xitop": s.setvi(l.it, s.itop) + elif n == "b": return (pc + 8 + l.imm * 8) & (0x3FF8 if s.vu else 0xFF8) + elif n == "bal": s.setvi(l.it, (pc + 16) // 8); return (pc + 8 + l.imm * 8) + elif n == "jr": return (V(l.is_) * 8) + elif n == "jalr": + t = V(l.is_) * 8; s.setvi(l.it, (pc + 16) // 8); return t + elif n in ("ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez"): + a, b = V(l.it), V(l.is_) + sb = b - 0x10000 if b & 0x8000 else b + c = {"ibeq": a == b, "ibne": a != b, "ibltz": sb < 0, "ibgtz": sb > 0, "iblez": sb <= 0, "ibgez": sb >= 0}[n] + if c: return pc + 8 + l.imm * 8 + else: + raise Exception("lower " + n) + return None + +def main(): + g, vu = sys.argv[2], int(sys.argv[3]) + bp = sys.argv[4] if len(sys.argv) > 4 else "vucap_before.bin" + ap = sys.argv[5] if len(sys.argv) > 5 else "vucap_after.bin" + code = load_images()[g] + reads_flags = any(decode_pair(*w)[1] is not None and decode_pair(*w)[1].name in + ("fsand", "fsor", "fseq", "fmand", "fmor", "fmeq") for w in code.values()) + st = parse(bp, vu); af = parse(ap, vu) + m = VU(st, code, vu) + m.run(st["pc"]) + while m.fpipe: _, m.mac, m.status = m.fpipe.pop(0) + while m.cpipe: _, m.clip = m.cpipe.pop(0) + if m.qp: m.q = m.qp[1] + diffs = 0 + for r in range(32): + for c in range(4): + if int(m.vf[r][c]) != int(af["vf"][r][c]): + diffs += 1 + if diffs < 30: print("vf%d.%s ref %08X C %08X" % (r, XYZW[c], int(m.vf[r][c]), int(af["vf"][r][c]))) + for r in range(16): + if m.vi[r] != af["vi"][r]: diffs += 1; print("vi%d ref %04X C %04X" % (r, m.vi[r], af["vi"][r])) + for c in range(4): + if int(m.acc[c]) != int(af["acc"][c]): diffs += 1; print("acc.%s ref %08X C %08X" % (XYZW[c], int(m.acc[c]), int(af["acc"][c]))) + for nm in ("q", "clip") + (("status", "mac") if reads_flags else ()): + if getattr(m, nm) != af[nm]: diffs += 1; print("%s ref %08X C %08X" % (nm, getattr(m, nm), af[nm])) + md = [k for k in range(0, len(m.mem), 4) if m.mem[k:k+4] != af["mem"][k:k+4]] + if md: + diffs += len(md) + print("memory differs at %d words, first at qw 0x%X" % (len(md), md[0] // 16)) + for k in md[:10]: + print(" qw %03X.%s ref %08X C %08X" % (k // 16, XYZW[(k // 4) & 3], struct.unpack_from("/rac1_wrapper.cfg`): + table of contents at LBA 1500, the level's header, its data range, whose + first word is the overlay's offset. An OpenRAC dump in the decomp repo + (`baserom/overlays/level_NN/`) is used instead when present. These are + game data; they stay in `build/`. + +## Which code is which + +`config/overlays/functions.tsv` lists every function of every level with +its places (`NN:ADDR`). For level NN the build uses those with a place in +NN, at that place: + +| Function | Built from | +|---|---| +| shared / level (`func_LNN_X`) with C in `src/overlays/shared` or `src/overlays/lNN_*` | that C (gen_host.py `--level`) | +| the same, `INCLUDE_ASM`, or C with inline asm | the level's bytes (recomp.py `--level`) | +| shared, canonical level another one (`func_L00_X` in level 01) | the level's bytes (its C names level 00's data) | +| exe kind (`func_X`, a copy of an executable game function) | the level's bytes: the copy's relocations are the level's own (data at the level's addresses, calls to the level's functions), which the executable's C cannot know | +| exe kind with a host version (`host/overrides/func_X.c`: memcpy-likes, WAD unpack, movie player, sound init) | that host C, compiled again for the level | +| exe kind the host provides in `host/src` (`func_001F9988`) | the same host function | +| a table entry inside a function (InitCamera_7 at 0x2EDB68 in level 00), a `jal` target no catalogued function starts at | the level's bytes, from that address (`func_lNN_entry_ADDR`) | +| a host function the catalogue does not place (level 00's busy wait at 0x1FEFC8, catalogued as nops plus `func_L00_001FEFE0`) | found by the executable function's bytes; the host function | +| a function at several places | each place translated on its own (`func_X__lNNatADDR`); C is shared | + +The level's C is only right for the level it was written for: a shared +function's C names its data and callees by its canonical level +(`D_L00_...`). Level 00 is the canonical level of everything it has, so all +its C applies. In another level, functions whose canonical level is not +that level come from its bytes (until C with relocations mapped from the +bytes, see Next). + +Assembly-only functions have no definition to take a signature from, so +the generator makes one of their declarations the reference (the longest +plain `extern` prototype), puts every other prototype in its float/int +order and gives recomp.py's C entry the same signature: the EE passes +floats and integers in separate registers, so `f(float, void *)` in one +file and `f(void *, float)` in another are the same call there, not on +x86. + +## Names: one header per level + +The executable and every level link into one program, and their names +overlap (the same `func_X` is the executable's function and a different +copy in each level; a shared `func_L00_X` exists in 19 levels). So the +level pass gives every name it defines or places the suffix `_lNN`: +`gen/lNN/lNN_names.h` (`#define func_L00_X func_L00_X_l00`, the same for +each main-segment `func_X`, each level data name `D_L00_X`, each alias) is +forced into every C file of the level (`-include`), and asm labels +(`__asm__("D_L00_X")`, `SDATA(...)`) are rewritten to match. Names the +executable pass provides with the same value (the core segment, core data +below 0x15F000) are left alone and resolve to the executable's. The +generator's own passes work on the original names; only the compiler sees +the suffixes. Aliases from the generator's passes (`__h1`, `__imp`) carry +the level too (`__l00h1`). + +Level outputs in `build//gen/lNN/`: + +| File | What | +|---|---| +| `overlays_*.c` | the level's C (other levels' functions left out) | +| `asm_stubs.c`, `missing.c` | stubs: the first are replaced by recomp.py; the second log calls to names the level's C uses that this level's program does not have | +| `rc_funcs_*.c`, `rc_wrappers.c`, `rc_thunks.c`, `rc_table.c` | translated functions (`rcNN_` prefix), their C entries, thunks into C, the dispatch table `rcNN_table` (with the core's translated functions and thunks) | +| `tramp_table.c` | `uya_tramp_table_lNN`: every function of the level at every place | +| `abs_syms.s` | the level's data and other by-address names (with the suffix) | +| `sigs.json`, `asm_entries.json`, `overrides.txt`, `funcs.tsv` | for recomp.py and build.py | + +## Fixes made along the way (both passes) + +- **Function values**: a function named as a value (callback argument, + pointer stored in a moby, `m->f74 = func_L00_0028F458`) is its PS2 + address on the PS2. In level C it now is too (`((__typeof__(f) *)0xADDR)`), + so translated code and tables get an address `rc_dispatch` knows, and C + calls through it still work (trampoline). Level pass only. +- **Labeled prototypes** (`extern void f_x(...) __asm__("f")`) now get the + float/int order fix as well (only the order: they are deliberate views). +- **Thunks** pass the extra `uya_aN` arguments of extended definitions. +- **Quadword accesses** (`lq`/`sq`/`lqc2`/`sqc2`) of translated code keep the + EE's masking of the low 4 bits, except on the x86 stack, where C locals + are not 16-byte aligned (libvu0's matrix functions on C locals read the + wrong data before). A side effect: the executable's attract loop (a run + without `UYA_LEVEL`) now gets to its moby drawing (`func_001EE9F8`, a + hand-written loop over scratchpad data that toSPR DMA brings in) and + stays there after about 16 frames (not investigated yet); with + `UYA_LEVEL=0@1` the level is asked for before. +- **recomp.py**: a function whose last instruction has a delay slot reads it + from the next word; in level mode a function that runs off its end + continues into the next one; libgcc (0x11DF10-0x1207B8) is translated + (level code calls helpers the executable's C does not). + +## Testing + +`UYA_LEVEL="0@1"` asks for level 0 at vsync 1, as the title menu's "go to +level" (`func_0022F4A0`) does, so a run reaches ParseBin without the menu. +`UYA_VSYNCS=N` ends a run at vsync N with the list of missing functions. + +``` +RAC1_ISO=disc.iso UYA_SKIPMOVIES=20 UYA_LEVEL=0@1 UYA_VSYNCS=2500 ./rac1_wrapper SCES_509.16 +``` + +State on 2026-10-10: ParseBin loads level 00 at vsync 769 (after the +level's three opening movies), the entry point (`func_L00_002465F8`, +translated) runs the level start-up (loaders, cameras, mobys: C and +translated code mixed) and enters the level's frame loop; up to vsync 2500 +no level function traps. The first frame's moby updates then wait: the update +of moby class 834 (`func_L00_002DB278`) starts the scene's dialog stream +(`func_L00_00299B68`) and calls `sound_update` and vsync until the stream +state (the halfword at 0x0015182A) reaches 3. Since 2026-10-11 the host's 989snd +(host/src/rac1/snd989.c) plays the stream and answers "started" then "buffered", so the +state goes 1, 2, 3, the scene continues it (4) and the dialog plays to its end. Nothing is drawn (as in the executable: the +display set-up and VU1 are not there yet). + +## Next + +- **Exe-kind functions from C.** About 940 of level 00's functions are + copies of executable functions that have C in `src/game`; today they are + translated from bytes. Comparing each copy with the executable's bytes + instruction by instruction gives its relocation map (each `%hi/%lo` and + `$gp` reference, each `jal`): the executable's C can then be compiled for + the level with those names mapped (a per-function rename, as the header + does per level), and checked: a copy whose map is not consistent stays + translated. The same map makes shared C usable in levels other than its + canonical one. +- **Other levels**: `--levels 00,01,...` builds them (shared functions of + another canonical level from bytes); only level 00 has been run. +- The level's assembly-only functions shrink as OpenRAC matches them; the + build picks the C up by itself. diff --git a/games/rac1/host/include/rac1_common.h b/games/rac1/host/include/rac1_common.h new file mode 100644 index 0000000..5407a17 --- /dev/null +++ b/games/rac1/host/include/rac1_common.h @@ -0,0 +1,43 @@ +/* rac1_common.h: replaces OpenRAC's include/common.h when the game's C is + * built for a PC (gen_host.py copies it to gen/include/common.h). + * + * The host is a 32-bit x86 program with PS2 RAM mapped at its real addresses, + * so pointers are 4 bytes and structs keep their PS2 layout. `long` is + * rewritten to `long long` by gen_host.py (the EE's long is 64-bit); build + * with -malign-double so 64-bit fields keep 8-byte alignment. */ +#ifndef COMMON_H +#define COMMON_H + +#include "uya_host.h" +#include "names.h" + +/* Matching-only placement attributes: no meaning on the host. */ +#define NOT_SDA +#define MACRO_ADDR +/* SDATA(D_x) names a scalar by another C name: an asm label to the address symbol + * (expanded first: a level program's names carry its suffix, gen/lNN/lNN_names.h) */ +#define UYA_SYMSTR_(x) #x +#define UYA_SYMSTR(x) UYA_SYMSTR_(x) +#ifdef _WIN32 +#define SDATA(sym) __asm__("_" UYA_SYMSTR(sym)) +#else +#define SDATA(sym) __asm__(UYA_SYMSTR(sym)) +#endif + +/* The decomp's quadword copy and clear (lq/sq inline asm on the EE). */ +static __inline__ void qcopy(void *dst, void *src) { __builtin_memcpy(dst, src, 16); } +static __inline__ void qcopy_nc(void *dst, void *src) { __builtin_memcpy(dst, src, 16); } +static __inline__ void qzero(void *p) { __builtin_memset(p, 0, 16); } + +/* EE FPU division for C patched with it (tools/source_patches.py): a division + * by zero gives +-MAX (the sign of a XOR b), never inf or NaN. */ +static __inline__ float ee_fdiv(float a, float b) +{ + if (b == 0.0f) { + unsigned ua, ub; __builtin_memcpy(&ua, &a, 4); __builtin_memcpy(&ub, &b, 4); + unsigned r = ((ua ^ ub) & 0x80000000u) | 0x7F7FFFFFu; float f; __builtin_memcpy(&f, &r, 4); return f; + } + return a / b; +} + +#endif /* COMMON_H */ diff --git a/games/rac1/host/include/rac1_levels.h b/games/rac1/host/include/rac1_levels.h new file mode 100644 index 0000000..537f878 --- /dev/null +++ b/games/rac1/host/include/rac1_levels.h @@ -0,0 +1,33 @@ +/* rac1_levels.h: the level programs compiled into the wrapper (docs/LEVELS.md). + * + * Each level carries its own build of the game program; ParseBin copies it + * over the executable's main segment. build.py compiles the levels it is + * asked for (--levels) and lists them in gen/level_table.c; ParseBin + * (host/src/rac1/levels.c) finds the one being loaded there and switches the + * trampolines and the recompiled code's dispatch table to it. */ +#ifndef RAC1_LEVELS_H +#define RAC1_LEVELS_H +#include "uya_runtime.h" + +typedef struct RcCtx RcCtx; +typedef struct { uint32_t addr; void (*fn)(RcCtx *); } rac1_rc_row_t; + +typedef struct { + int id; /* level index (docs/OVERLAYS.md, Levels) */ + uint32_t entry; /* the entry point ParseBin returns */ + uint32_t text_addr, text_bytes; /* its text record: how a loaded program is recognised */ + const uya_tramp_t *tramps; /* uya_tramp_table_lNN */ + const int *tramp_count; + const rac1_rc_row_t *rc_table; /* rcNN_table: its recompiled functions and thunks by address */ + const int *rc_count; +} rac1_level_t; + +extern const rac1_level_t rac1_levels[]; +extern const int rac1_level_count; + +/* host/src/rac1/rc_runtime.c: the table rc_dispatch looks addresses up in */ +void rc_set_table(const rac1_rc_row_t *table, int count); +/* host/src/runtime.c */ +void uya_install_tramp_table(const uya_tramp_t *table, int count); + +#endif diff --git a/games/rac1/host/include/rc.h b/games/rac1/host/include/rc.h new file mode 100644 index 0000000..5415226 --- /dev/null +++ b/games/rac1/host/include/rc.h @@ -0,0 +1,162 @@ +/* rc.h: runtime for code produced by tools/recomp.py. + * + * recomp.py translates the game's assembly-only functions (MIPS R5900, with + * MMI and VU0 macro instructions) into C ahead of time. Each becomes a C + * function over an RcCtx (the EE's registers); it is compiled with the rest of + * the program, so nothing is translated or interpreted while the game runs. + * Memory is the PS2's own address space, mapped at the same addresses. */ +#ifndef RC_H +#define RC_H +#include +#include +#include + +/* the helpers below are tiny: always inline them (the generated code is built + * with -fno-builtin -fno-inline-functions, which otherwise leaves each one, + * and the memcpy in it, a call) */ +#define RC_INLINE static inline __attribute__((always_inline)) + +typedef int8_t s8; typedef uint8_t u8; typedef int16_t s16; typedef uint16_t u16; +typedef int32_t s32; typedef uint32_t u32; typedef int64_t s64; typedef uint64_t u64; +typedef float f32; + +typedef union { + u64 d[2]; s64 sd[2]; u32 w[4]; s32 sw[4]; u16 h[8]; s16 sh[8]; u8 b[16]; s8 sb[16]; +} R128; + +typedef struct RcCtx { + R128 r[32]; + R128 hi, lo; /* hi/lo in the low doubleword, hi1/lo1 in the high one */ + u32 sa; + f32 f[32]; + f32 facc; /* the EE FPU accumulator */ + int fcc; /* FPU condition */ +} RcCtx; + +typedef struct { + f32 vf[32][4]; + f32 acc[4]; + f32 q, i, p, r; + u32 vi[16]; + u32 clip, status, mac; + /* micro mode (tools/vu_recomp.py, host/include/vu.h): CMSAR0, where the + * last microprogram ended (MSCNT), and the results still in the pipeline: + * MAC/status and clip flags become visible 4 cycles after the op issues, + * Q 7 (13 for RSQRT) cycles after the DIV. */ + u32 cmsar0, tpc; + int susp; /* VU0: parked in a wait loop for the EE (tpc = where to resume) */ + u32 st_int, cl_int; + u32 fp_n, fp_h, fp_rdy[4], fp_mac[4], fp_st[4]; + u32 cp_n, cp_h, cp_rdy[4], cp_val[4]; + u32 q_pend, q_rdy; f32 q_new; +} RcVu0; +extern RcVu0 rc_vu0; + +/* the shared stack pointer for nested C <-> recompiled calls */ +extern u32 rc_sp; +#define RC_GP 0x00166D00u + +void rc_dispatch(RcCtx *c, u32 addr); /* call any function by PS2 address */ +void rc_vcallms(RcCtx *c, u32 addr); +void rc_vu0_resume(void); /* a VU0 microprogram (host/src/gfx/vu.c) */ +void vu_run(int vu, u32 pc); /* run the translated microprogram at pc */ +void rc_syscall(RcCtx *c); +void rc_break(u32 pc); +void rc_bad(u32 pc, const char *what); +void rc_enter(RcCtx *c); +u32 rc_cop0(int r); + +/* ---- memory: KSEG aliases of RAM (0x20000000-0x3FFFFFFF uncached/accelerated) fold back ---- */ +RC_INLINE uintptr_t RC_A(u32 a) { return (uintptr_t)((((a >> 28) == 2 || (a >> 28) == 3) && (a & 0x0FFFFFFFu) < 0x02000000u) ? (a & 0x0FFFFFFFu) : a); } +#define M8(a) (*(u8 *)RC_A(a)) +#define M16(a) (*(u16 *)RC_A(a)) +#define M32(a) (*(u32 *)RC_A(a)) +#define M64(a) (*(u64 *)RC_A(a)) +#define MF(a) (*(f32 *)RC_A(a)) +/* The EE's hardware registers (0x10000000-0x1000FFFF): a store that starts a + * DMA channel runs the transfer at once; FIFO writes go to the VIF/GIF. */ +void rc_io_write32(u32 a, u32 v); +void rc_io_write128(u32 a, const void *v); +RC_INLINE int rc_is_io(u32 a) { return (a & 0xFFFF0000u) == 0x10000000u; } +RC_INLINE void rc_sw(u32 a, u32 v) { if (rc_is_io(a)) rc_io_write32(a, v); else M32(a) = v; } +RC_INLINE void rc_sd(u32 a, u64 v) { if (rc_is_io(a)) { rc_io_write32(a, (u32)v); rc_io_write32(a + 4, (u32)(v >> 32)); } else M64(a) = v; } +/* Quadword accesses (lq, sq, lqc2, sqc2) ignore the low 4 address bits on the + * EE, and the game relies on it. The one exception on the host: a local array + * of the C code on the x86 stack, which is not 16-byte aligned as the EE's + * would be, is used where it is (rc_cstack_lo/hi: the x86 stack, set by + * rc_enter). */ +extern u32 rc_cstack_lo, rc_cstack_hi; +RC_INLINE uintptr_t RC_QA(u32 a) +{ + return RC_A(a - rc_cstack_lo < rc_cstack_hi - rc_cstack_lo ? a : (a & ~15u)); +} +RC_INLINE void rc_lq(R128 *d, u32 a) { __builtin_memcpy(d, (void *)RC_QA(a), 16); } +RC_INLINE void rc_sq(const R128 *s, u32 a) { if (rc_is_io(a)) rc_io_write128(a & ~15u, s); else __builtin_memcpy((void *)RC_QA(a), s, 16); } + +#define SE32(x) ((u64)(s64)(s32)(u32)(x)) + +/* unaligned loads/stores (little-endian EE) */ +RC_INLINE u64 rc_lwl(u64 rt, u32 a) { u32 k = (a & 3) * 8; u32 w = M32(a & ~3u); + u32 keep = k == 24 ? 0 : (0x00FFFFFFu >> k); return SE32(((u32)rt & keep) | (w << (24 - k))); } +RC_INLINE u64 rc_lwr(u64 rt, u32 a) { u32 k = (a & 3) * 8; u32 w = M32(a & ~3u); + if (k == 0) return SE32(w); + u32 keep = ~(0xFFFFFFFFu >> k); return (rt & 0xFFFFFFFF00000000ull) | (((u32)rt & keep) | (w >> k)); } +RC_INLINE void rc_swl(u64 rt, u32 a) { u32 k = (a & 3) * 8; u32 *p = (u32 *)RC_A(a & ~3u); + u32 keep = k == 24 ? 0 : (0xFFFFFF00u << k); *p = (*p & keep) | ((u32)rt >> (24 - k)); } +RC_INLINE void rc_swr(u64 rt, u32 a) { u32 k = (a & 3) * 8; u32 *p = (u32 *)RC_A(a & ~3u); + u32 keep = k == 0 ? 0 : (0x00FFFFFFu >> (24 - k)); *p = (*p & keep) | ((u32)rt << k); } +RC_INLINE u64 rc_ldl(u64 rt, u32 a) { u32 k = (a & 7) * 8; u64 w = M64(a & ~7u); + u64 keep = k == 56 ? 0 : (0x00FFFFFFFFFFFFFFull >> k); return (rt & keep) | (w << (56 - k)); } +RC_INLINE u64 rc_ldr(u64 rt, u32 a) { u32 k = (a & 7) * 8; u64 w = M64(a & ~7u); + u64 keep = k == 0 ? 0 : ~(0xFFFFFFFFFFFFFFFFull >> k); return (rt & keep) | (w >> k); } +RC_INLINE void rc_sdl(u64 rt, u32 a) { u32 k = (a & 7) * 8; u64 *p = (u64 *)RC_A(a & ~7u); + u64 keep = k == 56 ? 0 : (0xFFFFFFFFFFFFFF00ull << k); *p = (*p & keep) | (rt >> (56 - k)); } +RC_INLINE void rc_sdr(u64 rt, u32 a) { u32 k = (a & 7) * 8; u64 *p = (u64 *)RC_A(a & ~7u); + u64 keep = k == 0 ? 0 : (0x00FFFFFFFFFFFFFFull >> (56 - k)); *p = (*p & keep) | (rt << k); } + +/* ---- integer helpers ---- */ +RC_INLINE s32 rc_plzcw1(u32 x) { u32 s = x >> 31; s32 n = 0; for (int b = 31; b >= 0 && ((x >> b) & 1) == s; b--) n++; return n - 1; } +RC_INLINE s32 rc_clamp16(s32 v) { return v > 32767 ? 32767 : v < -32768 ? -32768 : v; } + +/* ---- floats ---- */ +RC_INLINE u32 rc_fbits(f32 f) { u32 u; __builtin_memcpy(&u, &f, 4); return u; } +RC_INLINE f32 rc_bitsf(u32 u) { f32 f; __builtin_memcpy(&f, &u, 4); return f; } +RC_INLINE s32 rc_cvtws(f32 f) { /* EE: truncates, saturates */ + if (f >= 2147483647.0f) return 0x7FFFFFFF; + if (f <= -2147483648.0f) return (s32)0x80000000u; + return (s32)f; +} +RC_INLINE f32 rc_fsqrt(f32 x) { return __builtin_sqrtf(__builtin_fabsf(x)); } +/* The EE FPU and the VUs have no infinities: a result that overflows is + * +-MAX (0x7F7FFFFF). Without this, MAX * MAX (FastArcSin of a huge value + * after a division by zero) becomes inf and then NaN, which a camera's + * matrices keep for good. */ +RC_INLINE f32 rc_fc(f32 x) { u32 b = rc_fbits(x); return ((b & 0x7F800000u) == 0x7F800000u) ? rc_bitsf((b & 0x80000000u) | 0x7F7FFFFFu) : x; } +RC_INLINE f32 rc_fdiv(f32 a, f32 b) { if (b == 0.0f) return (rc_fbits(a) ^ rc_fbits(b)) & 0x80000000u ? -3.4028235e38f : 3.4028235e38f; return a / b; } +RC_INLINE s32 rc_ftoi(f32 f, int shift) { return rc_cvtws(f * (f32)(1 << shift)); } +RC_INLINE f32 rc_itof(s32 i, int shift) { return (f32)i / (f32)(1 << shift); } + +/* COP2 macro mode: an FMAC op (ADD/SUB/MUL/MADD/MSUB/OPMULA/OPMSUB and their + * ACC forms) sets the MAC flags (cfc2 $vi17) from its result, per written + * component (Z bits 3..0 = x..w, S bits 7..4), the others cleared, and the + * status flag's Z/S/U/O bits (cfc2 $vi16) from them, with the sticky copies + * (bits 6..9). The collision code (coll_sphere, func_L00_001F10E0) tests the + * signs of a VSUB this way: `cfc2 $at, $vi17; andi $at, 0xE0`. */ +RC_INLINE void rc_vu0_flags(const f32 *v, u32 dest) +{ + u32 mac = 0; + for (int i = 0; i < 4; i++) { + if (!(dest & (8u >> i))) continue; + u32 b; __builtin_memcpy(&b, &v[i], 4); + if ((b & 0x7FFFFFFFu) == 0) mac |= 8u >> i; + if (b >> 31) mac |= 0x80u >> i; + if ((b & 0x7F800000u) == 0x7F800000u) mac |= 0x8000u >> i; /* overflow */ + } + rc_vu0.mac = mac; + u32 st = (mac & 0xF ? 1u : 0) | (mac & 0xF0 ? 2u : 0) | (mac & 0xF00 ? 4u : 0) | (mac & 0xF000 ? 8u : 0); + rc_vu0.status = (rc_vu0.status & ~0xFu) | st | (st << 6); +} + +#define VF(n) rc_vu0.vf[n] +#define VACC rc_vu0.acc +#endif diff --git a/games/rac1/host/include/uya_host.h b/games/rac1/host/include/uya_host.h new file mode 100644 index 0000000..901b429 --- /dev/null +++ b/games/rac1/host/include/uya_host.h @@ -0,0 +1,46 @@ +/* uya_host.h: replaces the decomp's common.h when frontbin's C is built for a PC. + * + * Memory model: the host is a 32-bit x86 program, so pointers are 4 bytes like + * on the EE and every struct keeps its PS2 layout. PS2 main RAM is mapped at its + * real addresses (0x00100000-0x01FFFFFF), so the decomp's globals (declared + * extern, placed by address through abs_syms.s) and its absolute constants + * point at real memory. long is rewritten to long long by gen_host.py (the + * EE's long is 64-bit); build with -malign-double so 64-bit fields keep 8-byte + * alignment. + */ +#ifndef UYA_HOST_H +#define UYA_HOST_H + +typedef signed char s8; +typedef unsigned char u8; +typedef signed short s16; +typedef unsigned short u16; +typedef signed int s32; +typedef unsigned int u32; +typedef signed long long s64; +typedef unsigned long long u64; +typedef float f32; +typedef double f64; + +/* The decomp's assembly-inclusion macros have no meaning on the host; + * gen_host.py removes their uses, these are a fallback. */ +#define INCLUDE_ASM(FOLDER, NAME) +#define INCLUDE_RODATA(FOLDER, NAME) +#define ASM_FUNC(FOLDER, NAME) +#define LINKER_REMNANT(FOLDER, NAME) +#define TEXT_PADDING(N) + +/* The EE's MMI plzcw on one word: leading bits equal to the sign bit, minus one. */ +static inline s32 uya_plzcw(u32 x) +{ + u32 s = x >> 31; + s32 n = 0; + for (int b = 31; b >= 0 && ((x >> b) & 1) == s; b--) n++; + return n - 1; +} + +/* A function the host does not implement yet: logs once per name. */ +void uya_unimpl(const char *name); +void uya_log(const char *fmt, ...); + +#endif diff --git a/games/rac1/host/include/uya_runtime.h b/games/rac1/host/include/uya_runtime.h new file mode 100644 index 0000000..50750de --- /dev/null +++ b/games/rac1/host/include/uya_runtime.h @@ -0,0 +1,61 @@ +/* uya_runtime.h: the host runtime's own interface (not seen by frontbin code). */ +#ifndef UYA_RUNTIME_H +#define UYA_RUNTIME_H +#include +#include + +/* PS2 address ranges the host maps at their real addresses. */ +#define PS2_RAM_LO 0x00100000u /* the game never touches the EE kernel area below 1 MB */ +#define PS2_RAM_HI 0x02000000u /* 32 MB of EE main RAM */ +#define PS2_SPR 0x70000000u /* scratchpad */ +#define PS2_SPR_SIZE 0x4000u +#define PS2_EE_REGS 0x10000000u /* timers, IPU, GIF, VIF0/1, DMA, INTC... */ +#define PS2_EE_REGS_SZ 0x00010000u +#define PS2_VU_MEM 0x11000000u /* VU0/VU1 code and data memory */ +#define PS2_VU_MEM_SZ 0x00010000u +#define PS2_GS_PRIV 0x12000000u /* GS privileged registers */ +#define PS2_GS_PRIV_SZ 0x00002000u + +typedef struct { uint32_t ps2_addr; void *host_fn; } uya_tramp_t; +extern const uya_tramp_t uya_tramp_table[]; +extern const int uya_tramp_count; + +/* platform layer */ +int plat_map_fixed(uint32_t addr, uint32_t size, int exec); /* 0 on success */ +int plat_install_trap_handler(void); /* calls into unmapped PS2 code */ +void plat_flush_icache(void *p, size_t n); +const char *plat_exe_dir(void); /* folder of the running program */ +void plat_at_exit(void); +uint64_t plat_time_us(void); /* monotonic clock */ +void plat_sleep_us(uint64_t us); +/* video output: show a w x h 32-bit image (bytes B, G, R, X); returns 0 once the window was closed */ +int plat_present(const uint32_t *rgbx, int w, int h); +void plat_movie_started(void); /* testing hook (Linux UYA_SKIPMOVIES) */ +int plat_key_skip(void); /* a skip key (Enter/Space/Esc) was pressed since the last call */ +void plat_poll(void); /* keep the window responsive; exits when it was closed */ +/* pad buttons held on the keyboard, PS2 bit order, 1 = pressed (see host/src/sdk/pad.c) */ +unsigned plat_pad_buttons(void); +/* sound: plays `frames` interleaved stereo 16-bit samples once; plat_audio_stop ends it early. + * `tag` names the dump file on Linux (UYA_DUMP). */ +void plat_audio_play(const int16_t *lr, size_t frames, int rate, const char *tag); +void plat_audio_stop(void); +/* continuous 48 kHz stereo output for the sound effects mixer (host/src/sdk/sound.c) */ +void plat_stream_write(const int16_t *lr, int frames); +int plat_stream_queued(void); /* frames written but not played yet */ +int uya_disc_read(uint32_t lsn, uint32_t count, void *buf); /* 0 on success */ +uint32_t uya_frame_count(void); /* vsyncs so far (host/src/sdk/graph.c) */ + +/* runtime */ +int uya_load_elf_sections(const char *path, const char *const *sections); +void uya_install_trampolines(void); +void uya_fill_code_traps(uint32_t lo, uint32_t hi); +void uya_log(const char *fmt, ...); +void uya_log_open(void); /* also log to rac1_log.txt next to the program */ +void uya_trap_call(uint32_t ps2_addr, uint32_t ret_addr, const uint32_t *args); /* from the trap handler */ +void uya_report(void); +int uya_main(int argc, char **argv); +int uya_disc_open(const char *path); +void uya_disc_boot(void); +void uya_boot(void); /* boot_elf's set-up before the menu (host/src/boot.c) */ + +#endif diff --git a/games/rac1/host/include/vu.h b/games/rac1/host/include/vu.h new file mode 100644 index 0000000..0031ea7 --- /dev/null +++ b/games/rac1/host/include/vu.h @@ -0,0 +1,146 @@ +/* vu.h: runtime for the VU microprograms translated by tools/vu_recomp.py. + * + * Each microprogram the game uploads (VU0 through VIF0, VU1 through VIF1) is + * translated to C at build time from the user's executable. A translation is a + * function over an RcVu0 (VU0 uses rc_vu0, the registers it shares with the + * EE's macro mode; VU1 has rc_vu1). Data memory is the real address range: + * VU0 at 0x11004000 (4 KB), VU1 at 0x1100C000 (16 KB). + * + * The VU pipeline is visible to these programs in three places and modelled + * here: MAC/status and clip flags written by an op are seen by flag reads 4 + * cycles later, and Q from DIV/SQRT (RSQRT) 7 (13) cycles later. The + * translator counts cycles (with the stalls it can see statically) in a local + * `cyc`; results are queued with the cycle they become visible at. */ +#ifndef VU_H +#define VU_H +#include "rc.h" + +extern RcVu0 rc_vu1; + +#ifndef VU_TRACE +#define VU_TRACE(pc) /* testing hook: each instruction pair's address */ +#endif + +/* VU data memory, by quadword address */ +#define VU0_MEM(qa) ((u32 *)(uintptr_t)(0x11004000u + (((u32)(qa) & 0xFFu) << 4))) +#define VU1_MEM(qa) ((u32 *)(uintptr_t)(0x1100C000u + (((u32)(qa) & 0x3FFu) << 4))) + +RC_INLINE u32 vu_u(f32 f) { u32 u; __builtin_memcpy(&u, &f, 4); return u; } +RC_INLINE f32 vu_f(u32 u) { f32 f; __builtin_memcpy(&f, &u, 4); return f; } + +/* MAC flag bits for one component (x = 0 .. w = 3) of a result */ +/* an FMAC result as the VU has it: no inf or NaN, +-MAX instead (counted: FPSLOG) */ +extern u32 vu_nonfinite; +RC_INLINE f32 vu_fc(f32 x) +{ + u32 b = vu_u(x); + if (__builtin_expect((b & 0x7F800000u) == 0x7F800000u, 0)) { vu_nonfinite++; b = (b & 0x80000000u) | 0x7F7FFFFFu; __builtin_memcpy(&x, &b, 4); } + return x; +} +RC_INLINE u32 vu_macbits(f32 r, int c) +{ + u32 u = vu_u(r), m = 0; + if ((u & 0x7FFFFFFFu) == 0) m |= 8u >> c; + if (u >> 31) m |= 0x80u >> c; + return m; +} + +/* ---- MAC/status flag pipeline ---- */ +RC_INLINE void vu_fl_sync(RcVu0 *v, u32 cyc) +{ + while (v->fp_n && (s32)(cyc - v->fp_rdy[v->fp_h]) >= 0) { + v->mac = v->fp_mac[v->fp_h]; v->status = v->fp_st[v->fp_h]; + v->fp_h = (v->fp_h + 1) & 3; v->fp_n--; + } +} +RC_INLINE void vu_fl_push(RcVu0 *v, u32 cyc, u32 mac) +{ + u32 st = (v->st_int & 0xFC0u) | ((mac & 0xF) ? 1 : 0) | ((mac & 0xF0) ? 2 : 0); + st |= (st & 3) << 6; + v->st_int = st; + if (v->fp_n == 4) { v->mac = v->fp_mac[v->fp_h]; v->status = v->fp_st[v->fp_h]; v->fp_h = (v->fp_h + 1) & 3; v->fp_n--; } + u32 k = (v->fp_h + v->fp_n) & 3; + v->fp_rdy[k] = cyc + 4; v->fp_mac[k] = mac; v->fp_st[k] = st; v->fp_n++; +} +RC_INLINE u32 vu_status(RcVu0 *v, u32 cyc) { vu_fl_sync(v, cyc); return v->status; } +RC_INLINE u32 vu_mac(RcVu0 *v, u32 cyc) { vu_fl_sync(v, cyc); return v->mac; } +RC_INLINE void vu_fsset(RcVu0 *v, u32 cyc, u32 imm) +{ + vu_fl_sync(v, cyc + 8); + v->status = (v->status & 0x3F) | (imm & 0xFC0); v->st_int = v->status; +} + +/* ---- clip flag pipeline ---- */ +RC_INLINE void vu_cl_sync(RcVu0 *v, u32 cyc) +{ + while (v->cp_n && (s32)(cyc - v->cp_rdy[v->cp_h]) >= 0) { + v->clip = v->cp_val[v->cp_h]; v->cp_h = (v->cp_h + 1) & 3; v->cp_n--; + } +} +RC_INLINE void vu_clipw(RcVu0 *v, u32 cyc, const f32 *fs, f32 w) +{ + f32 a = __builtin_fabsf(w); + u32 f = (fs[0] > a) | ((fs[0] < -a) << 1) | ((fs[1] > a) << 2) | ((fs[1] < -a) << 3) | ((fs[2] > a) << 4) | ((fs[2] < -a) << 5); + v->cl_int = ((v->cl_int << 6) | f) & 0xFFFFFF; + if (v->cp_n == 4) { v->clip = v->cp_val[v->cp_h]; v->cp_h = (v->cp_h + 1) & 3; v->cp_n--; } + u32 k = (v->cp_h + v->cp_n) & 3; + v->cp_rdy[k] = cyc + 4; v->cp_val[k] = v->cl_int; v->cp_n++; +} +RC_INLINE u32 vu_clip(RcVu0 *v, u32 cyc) { vu_cl_sync(v, cyc); return v->clip; } +RC_INLINE void vu_fcset(RcVu0 *v, u32 cyc, u32 imm) +{ + vu_cl_sync(v, cyc + 8); + v->clip = v->cl_int = imm & 0xFFFFFF; +} + +/* ---- Q ---- */ +RC_INLINE f32 vu_q(RcVu0 *v, u32 cyc) +{ + if (v->q_pend && (s32)(cyc - v->q_rdy) >= 0) { v->q = v->q_new; v->q_pend = 0; } + return v->q; +} +/* WAITQ, or a new DIV while one is in flight: stall until Q is written */ +RC_INLINE u32 vu_qwait(RcVu0 *v, u32 cyc) +{ + if (v->q_pend) { if ((s32)(cyc - v->q_rdy) < 0) cyc = v->q_rdy; v->q = v->q_new; v->q_pend = 0; } + return cyc; +} +RC_INLINE void vu_qset(RcVu0 *v, u32 cyc, int lat, f32 val) { v->q_new = val; v->q_pend = 1; v->q_rdy = cyc + lat; } + +/* end of a microprogram: everything in flight lands */ +RC_INLINE void vu_settle(RcVu0 *v) +{ + while (v->fp_n) { v->mac = v->fp_mac[v->fp_h]; v->status = v->fp_st[v->fp_h]; v->fp_h = (v->fp_h + 1) & 3; v->fp_n--; } + while (v->cp_n) { v->clip = v->cp_val[v->cp_h]; v->cp_h = (v->cp_h + 1) & 3; v->cp_n--; } + if (v->q_pend) { v->q = v->q_new; v->q_pend = 0; } + v->fp_h = v->cp_h = 0; +} + +RC_INLINE f32 vu_div(f32 a, f32 b) +{ + if (b == 0.0f) return ((vu_u(a) ^ vu_u(b)) & 0x80000000u) ? -3.4028235e38f : 3.4028235e38f; + return a / b; +} +RC_INLINE f32 vu_sqrt(f32 a) { return __builtin_sqrtf(__builtin_fabsf(a)); } +RC_INLINE f32 vu_rsqrt(f32 a, f32 b) +{ + f32 s = __builtin_sqrtf(__builtin_fabsf(b)); + if (s == 0.0f) return (vu_u(a) & 0x80000000u) ? -3.4028235e38f : 3.4028235e38f; + return a / s; +} +RC_INLINE s32 vu_ftoi(f32 f, f32 scale) +{ + f32 x = f * scale; + if (x >= 2147483647.0f) return 0x7FFFFFFF; + if (x <= -2147483648.0f) return (s32)0x80000000u; + if (x != x) return 0; + return (s32)x; +} +RC_INLINE u32 vu_itof(u32 i, f32 inv) { return vu_u((f32)(s32)i * inv); } + +/* host side (host/src/gfx/vu.c) */ +void vu_xgkick(u32 qaddr); +u32 vu_vif_top(int vu); +u32 vu_vif_itop(int vu); +void vu_bad(int vu, u32 pc, const char *what); +#endif diff --git a/games/rac1/host/overrides/func_0012DB68.c b/games/rac1/host/overrides/func_0012DB68.c new file mode 100644 index 0000000..f3619c2 --- /dev/null +++ b/games/rac1/host/overrides/func_0012DB68.c @@ -0,0 +1,3 @@ +/* 0x0012DB68 snd_StartSoundSystem (assembly in the decomp): binds the IOP sound driver, which is host code here (host/src/rac1/snd989.c). */ +#include "common.h" +s32 func_0012DB68(void) { uya_log("989snd: sound system started (host IOP driver)"); return 0; } diff --git a/games/rac1/host/overrides/func_0012DDC0.c b/games/rac1/host/overrides/func_0012DDC0.c new file mode 100644 index 0000000..4284cee --- /dev/null +++ b/games/rac1/host/overrides/func_0012DDC0.c @@ -0,0 +1,4 @@ +/* 0x0012DDC0 snd_FlushSoundCommands: delivers owed replies, finished bank loads and stream-safe CD reads; nothing stays queued (host/src/rac1/snd989.c) */ +#include "common.h" +s32 rac1_snd_flush(void); +s32 func_0012DDC0(void) { return rac1_snd_flush(); } diff --git a/games/rac1/host/overrides/func_0012DFB0.c b/games/rac1/host/overrides/func_0012DFB0.c new file mode 100644 index 0000000..6c26b90 --- /dev/null +++ b/games/rac1/host/overrides/func_0012DFB0.c @@ -0,0 +1,3 @@ +/* 0x0012DFB0 snd_GotReturns: replies are always in */ +#include "common.h" +s32 func_0012DFB0(void) { return 1; } diff --git a/games/rac1/host/overrides/func_0012E060.c b/games/rac1/host/overrides/func_0012E060.c new file mode 100644 index 0000000..9595268 --- /dev/null +++ b/games/rac1/host/overrides/func_0012E060.c @@ -0,0 +1,11 @@ +/* 0x0012E060 snd_BankLoadByLoc(lsn, offset): the IOP loads a sound bank from the disc. Returns a bank handle (0 on failure). */ +#include "common.h" +u32 rac1_snd_bank_load_lsn(s32 lsn, s32 off); +u32 func_0012E060(s32 lsn, s32 off) +{ + u32 h; + *(volatile s32 *)0x0015ED88 = 0; + h = rac1_snd_bank_load_lsn(lsn, off); + uya_log("989snd: bank at LSN %d (+%d): handle %08X", lsn, off, h); + return h; +} diff --git a/games/rac1/host/overrides/func_0012E1C8.c b/games/rac1/host/overrides/func_0012E1C8.c new file mode 100644 index 0000000..a608c8c --- /dev/null +++ b/games/rac1/host/overrides/func_0012E1C8.c @@ -0,0 +1,13 @@ +/* 0x0012E1C8 snd_BankLoadFromEE_CB(addr, callback, arg): command 0x57. The bank is loaded at once; as in the + * library, the reply word (the bank handle) lands in D_0015EE00 and snd_FlushSoundCommands calls the callback. */ +#include "common.h" +u32 rac1_snd_bank_load_ee(u32 addr); +void func_0012E1C8(s32 addr, void *cb, s64 arg) +{ + *(volatile s32 *)0x0015ED88 = 0; + if (*(volatile s32 *)0x0015EDC8 != 0) { uya_log("989snd: bank load from EE while another is pending"); return; } + *(volatile s32 *)0x0015EDD0 = (s32)cb; + *(volatile s64 *)0x0015EDD8 = arg; + *(volatile u32 *)0x0015EE00 = rac1_snd_bank_load_ee((u32)addr); + *(volatile s32 *)0x0015EDC8 = 1; +} diff --git a/games/rac1/host/overrides/func_0012E688.c b/games/rac1/host/overrides/func_0012E688.c new file mode 100644 index 0000000..b6b2a53 --- /dev/null +++ b/games/rac1/host/overrides/func_0012E688.c @@ -0,0 +1,4 @@ +/* 0x0012E688 snd_SendIOPCommandAndWait(cmd, size, data): to the host's sound driver (host/src/rac1/snd989.c) */ +#include "common.h" +s32 rac1_snd_command(s32 cmd, s32 size, const void *data); +s32 func_0012E688(s32 cmd, s32 size, void *data) { return rac1_snd_command(cmd, size, data); } diff --git a/games/rac1/host/overrides/func_0012E820.c b/games/rac1/host/overrides/func_0012E820.c new file mode 100644 index 0000000..4f850ac --- /dev/null +++ b/games/rac1/host/overrides/func_0012E820.c @@ -0,0 +1,9 @@ +/* 0x0012E820 snd_SendIOPCommandNoWait(cmd, size, data, callback, arg): the reply goes to the callback at the next flush */ +#include "common.h" +s32 rac1_snd_command(s32 cmd, s32 size, const void *data); +void rac1_snd_owe(void *fn, s64 arg, s32 result); +void func_0012E820(s32 cmd, s32 size, void *data, void *cb, s64 arg) +{ + s32 r = rac1_snd_command(cmd, size, data); + rac1_snd_owe(cb, arg, r); +} diff --git a/games/rac1/host/overrides/func_0012EB18.c b/games/rac1/host/overrides/func_0012EB18.c new file mode 100644 index 0000000..f1d85f0 --- /dev/null +++ b/games/rac1/host/overrides/func_0012EB18.c @@ -0,0 +1,3 @@ +/* 0x0012EB18 snd_SendCurrentBatch (assembly in the decomp): commands are handled when sent */ +#include "common.h" +void func_0012EB18(void) {} diff --git a/games/rac1/host/overrides/func_0012F4A8.c b/games/rac1/host/overrides/func_0012F4A8.c new file mode 100644 index 0000000..76b9ce0 --- /dev/null +++ b/games/rac1/host/overrides/func_0012F4A8.c @@ -0,0 +1,40 @@ +/* 0x0012F4A8 wad: reads level arg0's header (0x2434 bytes) from the sector in + * the table of contents (D_0013A548, 8 bytes per level) into D_0013A5E0. + * Same as the game's (src/core/wad.c), plus a testing switch: + * UYA_LEVEL=n (or n@v, see host/src/rac1/boot.c) makes the first level the + * game loads (level 0) level n instead, and sets the current level + * (D_0015EE84) to n to match. Without UYA_LEVEL nothing changes. */ +#include "common.h" +#include +#include +extern int func_00121750(int, int, void *, void *); +extern int func_00120F30(int); +extern void func_00122598(int); +extern int func_00121930(void); + +int func_0012F4A8(int arg0) +{ + static int used; + if (!used) { + const char *e = getenv("UYA_LEVEL"); + used = 1; + if (e && arg0 == 0) { + arg0 = atoi(e); + *(volatile s32 *)0x0015EE84 = arg0; + uya_log("UYA_LEVEL: loading level %d instead of level 0 (testing)", arg0); + } + } + char buf[0x2800]; + char hdr[4]; + const int *entry = (const int *)(uintptr_t)(0x0013A548 + arg0 * 8); + hdr[0] = 0x20; + hdr[1] = *(volatile unsigned char *)0x0015EE58; + hdr[2] = 0; + hdr[3] = 0; + do { + func_00121750(*entry, 5, buf, hdr); + while (func_00120F30(1) != 0) func_00122598(0); + } while (func_00121930() != 0); + for (unsigned i = 0; i < 0x2434; i++) ((char *)(uintptr_t)0x0013A5E0)[i] = buf[i]; + return 1; +} diff --git a/games/rac1/host/overrides/func_001F9908.c b/games/rac1/host/overrides/func_001F9908.c new file mode 100644 index 0000000..91d2d87 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9908.c @@ -0,0 +1,12 @@ +#include "common.h" +/* 0x001F9908 (fastfunc, hand-written): count down the timer *p. + * 0 stays 0 and returns 1; otherwise it goes down by one and the result is 2 + * when it reaches 0, else 0. */ +s32 func_001F9908(s32 *p) +{ + s32 x = *p; + if (x == 0) return 1; + x = (x > 1 ? x : 1) - 1; + *p = x; + return x > 0 ? 0 : 2; +} diff --git a/games/rac1/host/overrides/func_001F9938.c b/games/rac1/host/overrides/func_001F9938.c new file mode 100644 index 0000000..2a4cbd2 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9938.c @@ -0,0 +1,10 @@ +#include "common.h" +/* 0x001F9938 (fastfunc, hand-written): func_001F9908 on a 16-bit timer. */ +s32 func_001F9938(s16 *p) +{ + s32 x = *p; + if (x == 0) return 1; + x = (x > 1 ? x : 1) - 1; + *p = (s16)x; + return x > 0 ? 0 : 2; +} diff --git a/games/rac1/host/overrides/func_001F9968.c b/games/rac1/host/overrides/func_001F9968.c new file mode 100644 index 0000000..4ae90ca --- /dev/null +++ b/games/rac1/host/overrides/func_001F9968.c @@ -0,0 +1,4 @@ +#include "common.h" +/* 0x001F9968 (fastfunc, hand-written): 30 - plzcw(x), the index of the + * highest bit that differs from the sign bit. */ +s32 func_001F9968(u32 x) { return 30 - uya_plzcw(x); } diff --git a/games/rac1/host/overrides/func_001F9978.c b/games/rac1/host/overrides/func_001F9978.c new file mode 100644 index 0000000..e235124 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9978.c @@ -0,0 +1,3 @@ +#include "common.h" +/* 0x001F9978 (fastfunc, hand-written): `teq $0,$0`, the game's halt. */ +void func_001F9978(void) { uya_log("game halt (func_001F9978)"); } diff --git a/games/rac1/host/overrides/func_001F99B0.c b/games/rac1/host/overrides/func_001F99B0.c new file mode 100644 index 0000000..45ffe8a --- /dev/null +++ b/games/rac1/host/overrides/func_001F99B0.c @@ -0,0 +1,13 @@ +#include +#include +#include "common.h" +/* 0x001F99B0 (fastfunc, hand-written): FastMemSet, a 32-bit pattern over n bytes (at least one word). */ +void func_001F99B0(u32 *p, u32 v, s32 n) +{ + { /* testing: UYA_LOADLOG=1 logs the scrubs of freed load buffers (0xDEADBEEF) */ + static int on = -1; + if (on < 0) on = getenv("UYA_LOADLOG") != 0; + if (on && v == 0xDEADBEEFu) { uint32_t uya_frame_count(void); uya_log("v%u fill %08X-%08X with DEADBEEF", uya_frame_count(), (u32)(uintptr_t)p, (u32)(uintptr_t)p + n); } + } + do { *p++ = v; n -= 4; } while (n > 0); +} diff --git a/games/rac1/host/overrides/func_001F99D8.c b/games/rac1/host/overrides/func_001F99D8.c new file mode 100644 index 0000000..a8c03c2 --- /dev/null +++ b/games/rac1/host/overrides/func_001F99D8.c @@ -0,0 +1,7 @@ +#include "common.h" +/* 0x001F99D8 (fastfunc, hand-written): clear n bytes in quadwords (at least one). */ +void func_001F99D8(void *p, s32 n) +{ + u8 *q = p; + do { __builtin_memset(q, 0, 16); q += 16; n -= 16; } while (n > 0); +} diff --git a/games/rac1/host/overrides/func_001F9A00.c b/games/rac1/host/overrides/func_001F9A00.c new file mode 100644 index 0000000..1ef4ced --- /dev/null +++ b/games/rac1/host/overrides/func_001F9A00.c @@ -0,0 +1,7 @@ +#include "common.h" +/* 0x001F9A00 (fastfunc, hand-written): copy n bytes, by quadwords, words or + * bytes depending on alignment. */ +void func_001F9A00(void *dst, const void *src, s32 n) +{ + if (n > 0) __builtin_memmove(dst, src, (u32)n); +} diff --git a/games/rac1/host/overrides/func_001F9A98.c b/games/rac1/host/overrides/func_001F9A98.c new file mode 100644 index 0000000..bf92da3 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9A98.c @@ -0,0 +1,7 @@ +#include "common.h" +/* 0x001F9A98 (fastfunc, hand-written): copy n bytes in quadwords (at least one). */ +void func_001F9A98(void *dst, const void *src, s32 n) +{ + u8 *d = dst; const u8 *s = src; + do { __builtin_memcpy(d, s, 16); d += 16; s += 16; n -= 16; } while (n > 0); +} diff --git a/games/rac1/host/overrides/func_001F9AC0.c b/games/rac1/host/overrides/func_001F9AC0.c new file mode 100644 index 0000000..18a39c2 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9AC0.c @@ -0,0 +1,9 @@ +#include "common.h" +/* 0x001F9AC0 (fastfunc, hand-written): dst = a | b over n bytes, in quadwords (at least one). */ +void func_001F9AC0(u32 *dst, const u32 *a, const u32 *b, s32 n) +{ + do { + for (int i = 0; i < 4; i++) dst[i] = a[i] | b[i]; + dst += 4; a += 4; b += 4; n -= 16; + } while (n > 0); +} diff --git a/games/rac1/host/overrides/func_001F9B70.c b/games/rac1/host/overrides/func_001F9B70.c new file mode 100644 index 0000000..5859d3b --- /dev/null +++ b/games/rac1/host/overrides/func_001F9B70.c @@ -0,0 +1,3 @@ +#include "common.h" +/* 0x001F9B70 (fastfunc, hand-written): abs(x) */ +s32 func_001F9B70(s32 x) { return x < 0 ? -x : x; } diff --git a/games/rac1/host/overrides/func_001F9D78.c b/games/rac1/host/overrides/func_001F9D78.c new file mode 100644 index 0000000..970baa4 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9D78.c @@ -0,0 +1,8 @@ +#include "common.h" +/* 0x001F9D78 (fastfunc, VU0): do two spheres (xyz centre, w radius) overlap? + * 1 when the squared distance is less than the squared sum of the radii. */ +s32 func_001F9D78(const f32 *a, const f32 *b) +{ + f32 dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2], r = a[3] + b[3]; + return dx * dx + dy * dy + dz * dz - r * r < 0.0f ? 1 : 0; +} diff --git a/games/rac1/host/overrides/func_001F9E58.c b/games/rac1/host/overrides/func_001F9E58.c new file mode 100644 index 0000000..4006af0 --- /dev/null +++ b/games/rac1/host/overrides/func_001F9E58.c @@ -0,0 +1,14 @@ +#include "common.h" +/* 0x001F9E58 (fastfunc, VU0): clamp the length of src.xyz to max into dst. + * Returns 1 when it was scaled, 0 when it was already short enough (or zero); + * dst is only written when scaled. */ +s32 func_001F9E58(f32 *dst, const f32 *src, f32 max) +{ + f32 l2 = src[0] * src[0] + src[1] * src[1] + src[2] * src[2]; + f32 len = __builtin_sqrtf(l2); + if (len == 0.0f) return 0; + if (max - len > 0.0f) return 0; + f32 q = max / len; + dst[0] = src[0] * q; dst[1] = src[1] * q; dst[2] = src[2] * q; dst[3] = src[3]; + return 1; +} diff --git a/games/rac1/host/overrides/func_0020C468.c b/games/rac1/host/overrides/func_0020C468.c new file mode 100644 index 0000000..e9d9d10 --- /dev/null +++ b/games/rac1/host/overrides/func_0020C468.c @@ -0,0 +1,89 @@ +#include +#include +/* func_0020C468(src, dst): unpack a "WAD" compressed block (Insomniac's + * LZ77 variant, close to LZO1X) and return the unpacked size. + * + * Ported from the UYA Wrapper (UYA's copy is func_0039B760). RAC1's retail + * version is the same hand-written asm: it checks the "WAD" magic and streams + * the input through the scratchpad in 0x2000-byte halves by DMA. Here the + * input is read straight from memory; a far match with distance 0 and length + * above 1 is the encoder's "go to the next 0x2000-byte chunk" marker, which in + * a linear read means skipping to the next chunk boundary of the data (which + * starts 0x10 bytes into the block). */ +#include "common.h" + +s32 func_0020C468(u8 *src, u8 *dst) +{ + u8 *start = dst; + { /* testing: UYA_LOADLOG=1 */ + static int on = -1; + if (on < 0) on = getenv("UYA_LOADLOG") != 0; + if (on) { uint32_t uya_frame_count(void); const char *rc_stack_scan(void); uya_log("v%u WAD unpack %08X -> %08X: %.3s; stack%s", uya_frame_count(), (u32)(uintptr_t)src, (u32)(uintptr_t)dst, (const char *)src, rc_stack_scan()); } + } + if (src[0] != 'W' || src[1] != 'A' || src[2] != 'D') return 0; + u32 size = src[3] | (src[4] << 8) | (src[5] << 16) | ((u32)src[6] << 24); + u8 *data = src + 0x10, *in = data, *end = src + size; + u32 flag, n; + u8 *m; + + if (in[0] > 0x11) { /* leading literal run */ + n = *in++ - 0x11; + while (n--) *dst++ = *in++; + goto after_literals; + } + for (;;) { + /* state A: a literal run may come next */ + if (in >= end) break; + flag = *in++; + if (flag < 0x10) { + n = flag ? flag : *in++ + 0xF; + n += 3; + while (n--) *dst++ = *in++; +after_literals: + if (in >= end) break; + flag = *in++; + } + /* a match */ + for (;;) { + if (flag >= 0x40) { + m = dst - 1 - ((flag >> 2) & 7); + m -= *in++ << 3; + n = (flag >> 5) - 1; + } else if (flag >= 0x20) { + n = flag & 0x1F; + if (!n) n = *in++ + 0x1F; + m = dst - 1 - ((in[0] >> 2) + (in[1] << 6)); + in += 2; + } else if (flag >= 0x10) { + m = dst - ((flag & 8) << 11); + n = flag & 7; + if (!n) n = *in++ + 7; + m -= (in[0] >> 2) + (in[1] << 6); + in += 2; + if (m == dst) { + if (n != 1) { /* next-chunk marker */ + u32 off = (u32)(in - data); + in = data + ((off + 0x1FFF) & ~0x1FFFu); + goto state_a; + } + goto trailing; /* distance 0, length 1: no copy */ + } + m -= 0x4000; + } else { + uya_unimpl("func_0020C468: bad WAD stream"); + return (s32)(dst - start); + } + *dst++ = *m++; + *dst++ = *m++; + do { *dst++ = *m++; } while ((s32)--n > 0); +trailing: + n = in[-2] & 3; /* up to 3 literals after a match */ + if (!n) goto state_a; + while (n--) *dst++ = *in++; + if (in >= end) return (s32)(dst - start); + flag = *in++; + } +state_a:; + } + return (s32)(dst - start); +} diff --git a/games/rac1/host/overrides/func_0022DBE8.c b/games/rac1/host/overrides/func_0022DBE8.c new file mode 100644 index 0000000..b4ce877 --- /dev/null +++ b/games/rac1/host/overrides/func_0022DBE8.c @@ -0,0 +1,43 @@ +/* 0x0022DBE8: SoundMasterVolumeInit (sound.c, still assembly in the decomp). + * Ported from the assembly: clears the sound tables at 0x0013E650, starts + * 989snd, sets playback mode, reverb and voice ranges, derives the six master + * volumes from the option values at 0x0015EEEC/0x0015EEF0, starts the music + * streamer (func_002161E0, which also registers the CD callback the + * streaming loads wait on) and sets the volumes. */ +#include "common.h" + +extern void func_0012DB68(void); /* snd_StartSoundSystem */ +extern void func_0012E380(s32); /* snd_SetPlaybackMode */ +extern void func_0012E3C0(s32, s32); +extern void func_0012F0E8(s32, s32); /* snd_PreAllocReverbWorkArea */ +extern void func_0012E3F8(s32, s32, s32); /* snd_SetGroupVoiceRange */ +extern void func_0012E348(s32, s32); /* snd_SetMasterVolume */ +extern void func_002161E0(void); /* MusicStreamInit */ + +void func_0022DBE8(void) +{ + u8 *b = (u8 *)0x0013E650; + s32 vol, v8, v7; + __builtin_memset(b, 0, 0x40); + *(u32 *)(b + 0x40) = 0; + *(u32 *)(b + 0x44) = 0; + for (s32 k = 1; k <= 30; k++) { *(u32 *)(b + 0x70 * k) = 0; b[0x70 * k + 4] = 0; } + func_0012DB68(); + func_0012E380(*(s32 *)0x0015EEE8 == 0); + func_0012E3C0(0, 1); + func_0012F0E8(2, 4); + func_0012E3F8(1, 0x18, 0x2F); + func_0012E3F8(2, 0x18, 0x2F); + func_0012E3F8(4, 0x18, 0x2F); + vol = *(s32 *)0x0015EEF0; + v8 = vol * 8 / 10; + v7 = vol * 7 / 10; + *(s32 *)(b + 0x4C) = *(s32 *)0x0015EEEC; + *(s32 *)(b + 0x48) = v8; + *(s32 *)(b + 0x50) = v8; + *(s32 *)(b + 0x54) = v7; + *(s32 *)(b + 0x58) = v7; + *(s32 *)(b + 0x5C) = vol; + func_002161E0(); + for (s32 i = 0; i < 6; i++) func_0012E348(i, *(s32 *)(b + 0x48 + 4 * i)); +} diff --git a/games/rac1/host/overrides/func_0023B670.c b/games/rac1/host/overrides/func_0023B670.c new file mode 100644 index 0000000..faf4f81 --- /dev/null +++ b/games/rac1/host/overrides/func_0023B670.c @@ -0,0 +1,13 @@ +/* 0x0023B670: play a movie (src/game/movie/movie.c): (sector, size, buffer, + * player state, language). The Sony sample player it runs (IPU, threads, + * SPU, uncached buffers) is replaced by the host's MPEG-2 player + * (host/src/movie/player.c); the language picks the movie's sound track. */ +#include "common.h" +int uya_movie_play_track(u32 lsn, u32 bytes, int track); +s32 func_0023B670(s32 sector, s32 size, s32 buf, s32 state, s32 lang) +{ + (void)buf; (void)state; + u32 bytes = (u32)size < 0x40000u ? (u32)size * 2048u : (u32)size; /* the table gives sectors */ + uya_movie_play_track((u32)sector, bytes, lang); + return 0; +} diff --git a/games/rac1/host/src/gfx/gif.c b/games/rac1/host/src/gfx/gif.c new file mode 100644 index 0000000..d65cd87 --- /dev/null +++ b/games/rac1/host/src/gfx/gif.c @@ -0,0 +1,54 @@ +/* GIF data from VIF1 DIRECT (PATH2): drawn by the software GS (host/src/gs) and counted here. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include + +static unsigned long long prims[8], tags, regs_written[256], ad_addr[128], image_qw; + +void uya_gs_gif(const u32 *d, u32 qwc); + +void uya_gif_packet(const u32 *d, u32 qwc) +{ + u32 q = 0; + static int want = -1; + uya_gs_gif(d, qwc); + if (want < 0) want = getenv("UYA_GIFSTATS") != 0; /* testing: count tags and registers */ + if (!want) return; + while (q < qwc) { + const u32 *t = d + q * 4; + u64 lo = ((u64)t[1] << 32) | t[0], hi = ((u64)t[3] << 32) | t[2]; + u32 nloop = lo & 0x7FFF, eop = (lo >> 15) & 1, pre = (lo >> 46) & 1, prim = (lo >> 47) & 0x7FF; + u32 flg = (lo >> 58) & 3, nreg = (lo >> 60) & 0xF; + if (!nreg) nreg = 16; + tags++; + if (pre) prims[prim & 7]++; + q++; + if (flg == 0) { /* PACKED: one qword per register per loop */ + for (u32 l = 0; l < nloop; l++) + for (u32 r = 0; r < nreg; r++) { + u32 rg = (hi >> (r * 4)) & 0xF; + regs_written[rg]++; + if (q < qwc) { + const u32 *v = d + q * 4; + if (rg == 0xE) { ad_addr[v[2] & 0x7F]++; if ((v[2] & 0x7F) == 0) prims[v[0] & 7]++; } + if (rg == 0x0) prims[v[0] & 7]++; + } + q++; + } + } else if (flg == 1) { /* REGLIST: 64 bits per register */ + q += (nloop * nreg + 1) / 2; + } else { /* IMAGE */ + q += nloop; image_qw += nloop; + } + if (eop) break; + } +} + +void uya_gif_stats(void) +{ + uya_log("GIF tags %llu; prims: point %llu line %llu linestrip %llu tri %llu tristrip %llu fan %llu sprite %llu", + tags, prims[0], prims[1], prims[2], prims[3], prims[4], prims[5], prims[6]); + uya_log("PACKED regs: %llu A+D, %llu RGBAQ, %llu ST, %llu UV, %llu XYZF2, %llu XYZ2; image qwords %llu", + regs_written[0xE], regs_written[1], regs_written[2], regs_written[3], regs_written[4], regs_written[5], image_qw); + for (int a = 0; a < 128; a++) if (ad_addr[a]) uya_log(" A+D GS reg 0x%02X: %llu", a, ad_addr[a]); +} diff --git a/games/rac1/host/src/gfx/vif.c b/games/rac1/host/src/gfx/vif.c new file mode 100644 index 0000000..d906c71 --- /dev/null +++ b/games/rac1/host/src/gfx/vif.c @@ -0,0 +1,279 @@ +/* VIF0 and VIF1: the command streams DMA channels 0 and 1 (and the VIF FIFOs) + * carry to the vector units. + * + * UNPACK writes VU data memory (every vn/vl format, signed/unsigned, the + * STMASK/STROW/STCOL masking, the STCYCL write/skip cycles, the STMOD + * offset/difference modes and VIF1's double buffering with BASE/OFFSET/TOPS + * and the +TOPS flag). MPG uploads microcode (host/src/gfx/vu.c); MSCAL, + * MSCALF and MSCNT run the translated microprogram there (tools/vu_recomp.py) + * to completion: the VU programs end with XGKICKs that hand their GIF packets + * to the GS. DIRECT/DIRECTHL carry GIF packets for the GS (PATH2). The flushes + * have nothing to wait for, since everything before them has already run. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include "vu.h" +#include +#include + +void uya_gif_packet(const u32 *data, u32 qwc); +void vu_mpg(int vu, u32 addr, const u32 *w, u32 ninstr); +void vu_run(int vu, u32 pc); + +typedef struct { + int id; + u32 cl, wl, mode, mask, row[4], col[4]; + u32 base, ofst, tops, top, itops, itop, dbf, mark; + u32 code, need, got; /* the command whose data is still coming */ + u32 *buf, cap; +} Vif; + +static u32 buf0[0x1000], buf1[0x10000 * 4 + 16]; +static Vif vif[2] = { { .id = 0, .cl = 1, .wl = 1, .buf = buf0, .cap = sizeof buf0 / 4 }, + { .id = 1, .cl = 1, .wl = 1, .buf = buf1, .cap = sizeof buf1 / 4 } }; +static unsigned long long cmd_count[128], frames; + +static const char *cmd_name(u32 c) +{ + switch (c) { + case 0x00: return "NOP"; case 0x01: return "STCYCL"; case 0x02: return "OFFSET"; case 0x03: return "BASE"; + case 0x04: return "ITOP"; case 0x05: return "STMOD"; case 0x06: return "MSKPATH3"; case 0x07: return "MARK"; + case 0x10: return "FLUSHE"; case 0x11: return "FLUSH"; case 0x13: return "FLUSHA"; case 0x14: return "MSCAL"; + case 0x15: return "MSCALF"; case 0x17: return "MSCNT"; case 0x20: return "STMASK"; case 0x30: return "STROW"; + case 0x31: return "STCOL"; case 0x4A: return "MPG"; case 0x50: return "DIRECT"; case 0x51: return "DIRECTHL"; + } + return (c & 0x60) == 0x60 ? "UNPACK" : "?"; +} + +u32 vu_vif_top(int vu) { return vif[vu & 1].top; } +u32 vu_vif_itop(int vu) { return vif[vu & 1].itop; } + +/* vectors of an UNPACK of `num` writes that take data (the rest are filled) */ +static u32 data_vectors(const Vif *v, u32 num) +{ + u32 cl = v->cl, wl = v->wl ? v->wl : 1; + if (wl <= cl) return num; + return (num / wl) * cl + (num % wl < cl ? num % wl : cl); +} + +static u32 unpack_words(const Vif *v, u32 cmd, u32 num) +{ + u32 vn = (cmd >> 2) & 3, vl = cmd & 3; + u32 bits = vl == 3 ? 16 : (32 >> vl) * (vn + 1); + return (bits * data_vectors(v, num) + 31) / 32; +} + +/* element k (of the format's element size) of the data */ +static inline u32 elem(const u32 *d, u32 nwords, u32 vl, u32 k, int usn) +{ + switch (vl) { + case 0: return k < nwords ? d[k] : 0; + case 1: { if (k / 2 >= nwords) return 0; u16 h = ((const u16 *)d)[k]; return usn ? h : (u32)(s32)(s16)h; } + case 2: { if (k / 4 >= nwords) return 0; u8 b = ((const u8 *)d)[k]; return usn ? b : (u32)(s32)(s8)b; } + } + return 0; +} + + +/* the usual UNPACK: no mask, every write takes data (WL <= CL); the format is + * a constant here (see unpack()), so elem() comes down to a load */ +static inline __attribute__((always_inline)) void unpack_plain(Vif *v, u8 *mem, u32 qmask, u32 addr, u32 cl, u32 wl, + const u32 *d, u32 nwords, u32 num, const u32 vn, const u32 vl, u32 usn) +{ + for (u32 k = 0; k < num; k++) { + u32 qa = wl == cl ? addr + k : addr + (k / wl) * cl + k % wl, val[4]; + u32 e = k * (vn + 1); + switch (vn) { + case 0: val[0] = val[1] = val[2] = val[3] = elem(d, nwords, vl, e, usn); break; + case 1: val[0] = val[2] = elem(d, nwords, vl, e, usn); val[1] = val[3] = elem(d, nwords, vl, e + 1, usn); break; + default: for (int c = 0; c < 4; c++) val[c] = elem(d, nwords, vl, e + c, usn); break; + } + u32 *dst = (u32 *)(mem + ((qa & qmask) << 4)); + if (v->mode == 0) { dst[0] = val[0]; dst[1] = val[1]; dst[2] = val[2]; dst[3] = val[3]; } + else if (v->mode == 1) for (int c = 0; c < 4; c++) dst[c] = val[c] + v->row[c]; + else if (v->mode == 2) for (int c = 0; c < 4; c++) { v->row[c] += val[c]; dst[c] = v->row[c]; } + else for (int c = 0; c < 4; c++) dst[c] = val[c]; + } +} + +static void unpack(Vif *v, u32 code, const u32 *d, u32 nwords) +{ + u32 cmd = (code >> 24) & 0x7F, imm = code & 0xFFFF, num = (code >> 16) & 0xFF; + if (!num) num = 256; + u32 vn = (cmd >> 2) & 3, vl = cmd & 3, m = (cmd >> 4) & 1, usn = (imm >> 14) & 1; + u32 qmask = v->id ? 0x3FF : 0xFF; + u32 addr = imm & 0x3FF; + if (v->id && (imm & 0x8000)) addr += v->tops; + u8 *mem = (u8 *)(uintptr_t)(v->id ? 0x1100C000u : 0x11004000u); + u32 cl = v->cl, wl = v->wl ? v->wl : 1, di = 0; + if (!m && wl <= cl && vl != 3) { + switch (vn * 4 + vl) { +#define U(n, l) case n * 4 + l: unpack_plain(v, mem, qmask, addr, cl, wl, d, nwords, num, n, l, usn); return; + U(0, 0) U(0, 1) U(0, 2) U(1, 0) U(1, 1) U(1, 2) U(2, 0) U(2, 1) U(2, 2) U(3, 0) U(3, 1) U(3, 2) +#undef U + } + } + for (u32 k = 0; k < num; k++) { + u32 pos = k % wl, qa, has; + if (wl <= cl) { qa = addr + (k / wl) * cl + pos; has = 1; } + else { qa = addr + k; has = pos < cl; } + u32 val[4] = { 0, 0, 0, 0 }; + if (has) { + if (vl == 3) { /* V4-5: RGBA 5:5:5:1 */ + u32 h = di / 2 < nwords ? ((const u16 *)d)[di] : 0; + val[0] = (h & 0x1F) << 3; val[1] = ((h >> 5) & 0x1F) << 3; val[2] = ((h >> 10) & 0x1F) << 3; val[3] = ((h >> 15) & 1) << 7; + } else { + u32 e = di * (vn + 1); + switch (vn) { + case 0: val[0] = val[1] = val[2] = val[3] = elem(d, nwords, vl, e, usn); break; + case 1: val[0] = val[2] = elem(d, nwords, vl, e, usn); val[1] = val[3] = elem(d, nwords, vl, e + 1, usn); break; + case 2: for (int c = 0; c < 4; c++) val[c] = elem(d, nwords, vl, e + c, usn); break; /* w: the next element */ + case 3: for (int c = 0; c < 4; c++) val[c] = elem(d, nwords, vl, e + c, usn); break; + } + } + di++; + } + u32 *dst = (u32 *)(mem + ((qa & qmask) << 4)); + u32 r = pos < 3 ? pos : 3; + for (int c = 0; c < 4; c++) { + u32 mk = m ? (v->mask >> ((r * 4 + c) * 2)) & 3 : (has ? 0 : 1); + switch (mk) { + case 0: + if (v->mode == 1) dst[c] = val[c] + v->row[c]; + else if (v->mode == 2) { v->row[c] += val[c]; dst[c] = v->row[c]; } + else dst[c] = val[c]; + break; + case 1: dst[c] = v->row[c]; break; + case 2: dst[c] = v->col[r]; break; + default: break; + } + } + } +} + +static void start(Vif *v, u32 pc) +{ + if (v->id) { + v->top = v->tops; + if (v->dbf) { v->tops = v->base; v->dbf = 0; } + else { v->tops = (v->base + v->ofst) & 0x3FF; v->dbf = 1; } + } + v->itop = v->itops; + vu_run(v->id, pc); +} + +static void bad_cmd(Vif *v, u32 code) +{ + static u32 seen[2][4]; + u32 c = (code >> 24) & 0x7F; + if (seen[v->id][c / 32] & (1u << (c % 32))) return; + seen[v->id][c / 32] |= 1u << (c % 32); + uya_log("VIF%d: command 0x%02X (%08X) not handled", v->id, c, code); +} + +/* run a command once all its data (d, n words) is there */ +int uya_vif_log; /* testing: UYA_DMALOG=n also lists send n's VIF commands */ + +static u32 hist[64][3], hist_n; /* testing: the last VIF1 commands (code, TOPS, first data word) */ +void uya_vif1_dump_hist(void) +{ + uya_log("VIF1: BASE %03X OFFSET %03X TOPS %03X TOP %03X DBF %u ITOP %03X; last commands:", vif[1].base, vif[1].ofst, vif[1].tops, vif[1].top, vif[1].dbf, vif[1].itop); + for (u32 i = 0; i < 64; i++) { u32 k = (hist_n + i) & 63; if (hist[k][0] || hist[k][1]) uya_log(" %-8s %08X tops %03X data0 %08X", cmd_name((hist[k][0] >> 24) & 0x7F), hist[k][0], hist[k][1], hist[k][2]); } +} + +static void exec(Vif *v, u32 code, const u32 *d, u32 n) +{ + u32 cmd = (code >> 24) & 0x7F, imm = code & 0xFFFF, num = (code >> 16) & 0xFF; + if (v->id) { cmd_count[cmd]++; hist[hist_n & 63][0] = code; hist[hist_n & 63][1] = v->tops; hist[hist_n & 63][2] = n ? d[0] : 0; hist_n++; } + if (uya_vif_log) uya_log(" VIF%d %-8s %08X (%u data words)", v->id, cmd_name(cmd), code, n); + switch (cmd) { + case 0x00: case 0x06: case 0x10: case 0x11: case 0x13: break; + case 0x01: v->cl = imm & 0xFF; v->wl = (imm >> 8) & 0xFF; break; + case 0x02: v->ofst = imm & 0x3FF; v->dbf = 0; v->tops = v->base; break; + case 0x03: v->base = imm & 0x3FF; break; + case 0x04: v->itops = imm & 0x3FF; break; + case 0x05: v->mode = imm & 3; break; + case 0x07: v->mark = imm; break; + case 0x14: case 0x15: start(v, imm * 8); break; + case 0x17: start(v, v->id ? rc_vu1.tpc : rc_vu0.tpc); break; + case 0x20: v->mask = d[0]; break; + case 0x30: memcpy(v->row, d, 16); break; + case 0x31: memcpy(v->col, d, 16); break; + case 0x4A: vu_mpg(v->id, imm * 8, d, num ? num : 256); break; + case 0x50: case 0x51: + if (v->id) { + extern const char *uya_gs_src; uya_gs_src = "PATH2 (VIF1 DIRECT)"; + static int left = -1; /* testing: UYA_BADTEX=n logs DIRECT packets with a TEX0 whose top half looks like a pointer */ + if (left < 0) { const char *e = getenv("UYA_BADTEXV"); left = e ? atoi(e) : 0; } + if (left > 0) + for (u32 k = 0; k + 3 < n; k += 4) + if ((d[k + 2] & 0xFF) == 6 && (d[k + 1] >> 16) >= 0xFF00) { + left--; + uya_log("DIRECT packet at %08X (%u qwords) has TEX0 %08X%08X at qword %u:", (u32)(uintptr_t)d, n / 4, d[k + 1], d[k], k / 4); + for (u32 j = 0; j < n && j < 64; j += 4) uya_log(" %08X: %08X %08X %08X %08X", (u32)(uintptr_t)(d + j), d[j], d[j + 1], d[j + 2], d[j + 3]); + break; + } + uya_gif_packet(d, n / 4); + } + else bad_cmd(v, code); + break; + default: + if ((cmd & 0x60) == 0x60) unpack(v, code, d, n); + else bad_cmd(v, code); + break; + } +} + +/* words of data that follow a command */ +static u32 data_words(Vif *v, u32 code) +{ + u32 cmd = (code >> 24) & 0x7F, imm = code & 0xFFFF, num = (code >> 16) & 0xFF; + switch (cmd) { + case 0x20: return 1; + case 0x30: case 0x31: return 4; + case 0x4A: return (num ? num : 256) * 2; + case 0x50: case 0x51: return (imm ? imm : 0x10000) * 4; + } + if ((cmd & 0x60) == 0x60) return unpack_words(v, cmd, num ? num : 256); + return 0; +} + +static void feed(Vif *v, const u32 *w, u32 n) +{ + u32 i = 0; + while (i < n) { + if (v->need) { + u32 take = v->need < n - i ? v->need : n - i; + if (!v->got && take == v->need) { /* all of it here: no copy */ + v->need = 0; + exec(v, v->code, w + i, take); + i += take; + continue; + } + for (u32 k = 0; k < take; k++) if (v->got + k < v->cap) v->buf[v->got + k] = w[i + k]; + v->got += take; v->need -= take; i += take; + if (!v->need) { u32 g = v->got < v->cap ? v->got : v->cap; v->got = 0; exec(v, v->code, v->buf, g); } + continue; + } + u32 code = w[i++]; + u32 nd = data_words(v, code); + if (!nd) { exec(v, code, 0, 0); continue; } + v->code = code; v->need = nd; v->got = 0; + } +} + +void uya_vif0_words(const u32 *w, u32 n) { feed(&vif[0], w, n); } +void uya_vif0_data(const u32 *w, u32 qwc) { feed(&vif[0], w, qwc * 4); } +void uya_vif1_words(const u32 *w, u32 n) { feed(&vif[1], w, n); } +void uya_vif1_data(const u32 *w, u32 qwc) { feed(&vif[1], w, qwc * 4); } + +void uya_gfx_frame_stats(void) +{ + frames++; + if (frames == 3 || frames == 600 || frames == 3000) { + void vu_stats(void); + uya_log("VIF1 command counts after %llu sends:", frames); + for (int c = 0; c < 128; c++) + if (cmd_count[c]) uya_log(" %-8s %llu", cmd_name(c), cmd_count[c]); + vu_stats(); + } +} diff --git a/games/rac1/host/src/gfx/vu.c b/games/rac1/host/src/gfx/vu.c new file mode 100644 index 0000000..9d92dda --- /dev/null +++ b/games/rac1/host/src/gfx/vu.c @@ -0,0 +1,278 @@ +/* VU0/VU1 micro mode: which translated microprogram is in micro memory, and + * running it. + * + * The game uploads microcode with MPG (VIF0 for VU0, VIF1 for VU1). The bytes + * land in micro memory at their real address (VU0 0x11000000, 4 KB; VU1 + * 0x11008000, 16 KB). tools/vu_recomp.py translated every microprogram image + * in the executable's .vutext to C at build time and kept its bytes: after an + * upload, every known upload block that overlaps it is compared with micro + * memory, so a start address (vcallms / MSCAL / MSCNT) maps to the image whose + * blocks are all in place. Microcode that matches nothing is reported once + * with its address and a hash (UYA_VUDUMP=dir also saves it), so it can be + * added to the build. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include "vu.h" +#include +#include +#include + +typedef struct { u32 vma, size; const u32 *words; } VuBlock; +typedef struct { const char *name; int vu; void (*fn)(RcVu0 *, u32); int nblk; const VuBlock *blk; } VuImage; +extern const VuImage vu_images[]; +extern const int vu_image_count; + +RcVu0 rc_vu1 = { .vf = { [0] = {0, 0, 0, 1} } }; + +#define MAX_IMG 64 +static u8 blk_ok[MAX_IMG][16]; /* block b of image i is in micro memory */ +static int cur_img[2][0x4000 / 8]; /* image last found for a start address, -1 none */ +static int cache_valid[2]; + +static u8 *micro(int vu) { return (u8 *)(uintptr_t)(vu ? 0x11008000u : 0x11000000u); } +u32 vu_nonfinite; /* VU FMAC results that overflowed to inf/NaN (made +-MAX: vu_fc, vu.h) */ +static u32 micro_size(int vu) { return vu ? 0x4000 : 0x1000; } + +void vu_bad(int vu, u32 pc, const char *what) +{ + static int n; + if (n++ < 32) uya_log("VU%d microprogram: %s at 0x%04X", vu, what, pc); +} + +static u32 fnv(const u8 *p, u32 n) { u32 h = 2166136261u; while (n--) { h ^= *p++; h *= 16777619u; } return h; } + +/* MPG: ninstr instructions to micro memory at byte address addr */ +void vu_mpg(int vu, u32 addr, const u32 *w, u32 ninstr) +{ + u8 *m = micro(vu); + u32 msz = micro_size(vu), bytes = ninstr * 8; + for (u32 i = 0; i < ninstr; i++) memcpy(m + ((addr + i * 8) & (msz - 1)), w + i * 2, 8); + /* re-check every known block that overlaps the upload */ + int covered = 0; + for (int i = 0; i < vu_image_count && i < MAX_IMG; i++) { + const VuImage *im = &vu_images[i]; + if (im->vu != vu) continue; + for (int b = 0; b < im->nblk && b < 16; b++) { + const VuBlock *bl = &im->blk[b]; + if (bl->vma < addr + bytes && addr < bl->vma + bl->size) { + blk_ok[i][b] = memcmp(m + bl->vma, bl->words, bl->size) == 0; + if (blk_ok[i][b] && bl->vma <= addr && addr + bytes <= bl->vma + bl->size) covered = 1; + } + } + } + cache_valid[vu] = 0; + if (!covered) { + static u32 seen[64]; static int nseen; + u32 h = fnv((const u8 *)w, bytes); + int k; + for (k = 0; k < nseen && seen[k] != h; k++) ; + if (k == nseen) { + if (nseen < 64) seen[nseen++] = h; + uya_log("VU%d: microcode uploaded at 0x%04X (%u instructions, hash %08X) is not one the build translated", vu, addr, ninstr, h); + const char *dir = getenv("UYA_VUDUMP"); + if (dir) { + char path[512]; snprintf(path, sizeof path, "%s/vu%d_%04X_%08X.bin", dir, vu, addr, h); + FILE *f = fopen(path, "wb"); if (f) { fwrite(w, 1, bytes, f); fclose(f); } + } + } + } +} + +/* The image to run from pc: the one whose upload containing pc is in micro + * memory, preferring one that is there completely (a later upload may have + * replaced a part of an image the program at pc never reaches, e.g. image + * 28259's stub at 0x0000 under the 0x0000-0x0580 one). */ +static int find_image(int vu, u32 pc) +{ + if (!cache_valid[vu]) { memset(cur_img[vu], 0xFF, sizeof cur_img[vu]); cache_valid[vu] = 1; } + int *slot = &cur_img[vu][(pc & (micro_size(vu) - 1)) / 8]; + if (*slot >= 0) return *slot; + int part = -1; + for (int i = 0; i < vu_image_count && i < MAX_IMG; i++) { + const VuImage *im = &vu_images[i]; + if (im->vu != vu) continue; + int has = 0, ok = 1; + for (int b = 0; b < im->nblk && b < 16; b++) { + const VuBlock *bl = &im->blk[b]; + if (pc >= bl->vma && pc < bl->vma + bl->size && blk_ok[i][b]) has = 1; + if (!blk_ok[i][b]) ok = 0; + } + if (has && ok) { *slot = i; return i; } + if (has && part < 0) part = i; + } + return *slot = part; +} + +static unsigned long long runs[2], kicks, kick_qw; +static unsigned long long img_runs[MAX_IMG]; +static u32 *pc_runs[MAX_IMG]; /* runs per start address, for the statistics */ +static const char *cur_name = ""; static u32 cur_pc; + +/* Testing: UYA_VUCAPTURE=image,n saves the state (start address, registers, + * data memory) before and after the n-th run of that image to + * vucap_before.bin / vucap_after.bin in the working directory, to replay it + * against a reference interpreter (tools/vu_refcheck.py). The file ends + * with VIF TOP and ITOP. */ +static void capture(const char *path, int vu, u32 pc, const RcVu0 *v) +{ + FILE *f = fopen(path, "wb"); + if (!f) return; + fwrite(&pc, 4, 1, f); fwrite(v, sizeof *v, 1, f); + fwrite((void *)(uintptr_t)(vu ? 0x1100C000u : 0x11004000u), 1, vu ? 0x4000 : 0x1000, f); + u32 tops[2] = { vu_vif_top(vu), vu_vif_itop(vu) }; + fwrite(tops, 4, 2, f); + fclose(f); +} + +static void xgkick_flush(void); +unsigned long long uya_t_vu; +static void vu_run_(int vu, u32 pc); +extern int uya_timing; /* host/src/gs/gs.c: UYA_FPSLOG */ +void vu_run(int vu, u32 pc) +{ + if (uya_timing <= 0) { vu_run_(vu, pc); return; } + unsigned lo, hi, lo2, hi2; __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi)); + vu_run_(vu, pc); + __asm__ volatile("rdtsc" : "=a"(lo2), "=d"(hi2)); + uya_t_vu += (((unsigned long long)hi2 << 32) | lo2) - (((unsigned long long)hi << 32) | lo); +} +static void vu_run_(int vu, u32 pc) +{ + RcVu0 *v = vu ? &rc_vu1 : &rc_vu0; + pc &= micro_size(vu) - 8; + int i = find_image(vu, pc); + if (i < 0) { + static u32 seen[2][0x4000 / 8 / 32]; + u32 k = pc / 8; + if (!(seen[vu][k / 32] & (1u << (k % 32)))) { + seen[vu][k / 32] |= 1u << (k % 32); + uya_log("VU%d: no translated microprogram for start address 0x%04X", vu, pc); + } + return; + } + runs[vu]++; + img_runs[i]++; + if (!pc_runs[i]) pc_runs[i] = calloc(0x800, sizeof(u32)); + if (pc_runs[i]) pc_runs[i][(pc / 8) & 0x7FF]++; + cur_name = vu_images[i].name; cur_pc = pc; + static int want = -2, seen; static char wname[32]; static unsigned wpc = 0xFFFFFFFFu; + if (want == -2) { const char *e = getenv("UYA_VUCAPTURE"); want = -1; if (e && sscanf(e, "%31[^,],%d,%x", wname, &want, &wpc) < 2) want = -1; } + if (want >= 0 && !strcmp(wname, vu_images[i].name) && (wpc == 0xFFFFFFFFu || wpc == pc) && seen++ == want) { + capture("vucap_before.bin", vu, pc, v); + vu_images[i].fn(v, pc); + if (vu) xgkick_flush(); + capture("vucap_after.bin", vu, pc, v); + uya_log("VU capture of image %s run %d (from 0x%04X) saved", wname, want, pc); + return; + } + vu_images[i].fn(v, pc); + if (vu) xgkick_flush(); +} + +/* VU1 XGKICK: the GIF packet at quadword qaddr of VU1 data memory goes to the + * GS (PATH1). The packet is complete when the program kicks it. */ +void uya_gif_packet(const u32 *data, u32 qwc); +static u32 kbuf[0x400 * 4], kbuf_n, kbuf_q; +void uya_vu_dump_kick(void) +{ + void uya_vif1_dump_hist(void); + uya_log("current XGKICK: qw %03X, %u qwords, image %s from %04X; vi:", kbuf_q, kbuf_n, cur_name, cur_pc); + for (int k = 0; k < 16; k += 4) uya_log(" vi%-2d %04X %04X %04X %04X", k, rc_vu1.vi[k] & 0xFFFF, rc_vu1.vi[k + 1] & 0xFFFF, rc_vu1.vi[k + 2] & 0xFFFF, rc_vu1.vi[k + 3] & 0xFFFF); + for (u32 k = 0; k < kbuf_n && k < 48; k++) uya_log(" %03X: %08X %08X %08X %08X", (kbuf_q + k) & 0x3FF, kbuf[k * 4], kbuf[k * 4 + 1], kbuf[k * 4 + 2], kbuf[k * 4 + 3]); + uya_vif1_dump_hist(); +} +/* XGKICK starts the transfer; the GIF reads the packet while the program + * goes on, and programs rely on that: they finish the packet in the kick's + * own branch delay slot or end-of-program slot (image 221571 writes the last + * GIF tag's EOP there). So a kick is sent when the next kick comes or the + * microprogram ends. */ +static u32 kick_pending = 0xFFFFFFFFu; +static void xgkick_send(u32 qaddr); +void vu_xgkick(u32 qaddr) +{ + if (kick_pending != 0xFFFFFFFFu) xgkick_send(kick_pending); + kick_pending = qaddr & 0x3FF; +} +static void xgkick_flush(void) +{ + if (kick_pending != 0xFFFFFFFFu) { u32 q = kick_pending; kick_pending = 0xFFFFFFFFu; xgkick_send(q); } +} +static void xgkick_send(u32 qaddr) +{ + u32 *buf = kbuf; + const u32 *mem = (const u32 *)(uintptr_t)0x1100C000u; + u32 q = qaddr & 0x3FF, n = 0; + while (n < 0x400) { + const u32 *t = mem + q * 4; + u64 lo = ((u64)t[1] << 32) | t[0]; + u32 nloop = lo & 0x7FFF, eop = (lo >> 15) & 1, flg = (lo >> 58) & 3, nreg = (lo >> 60) & 15; + if (!nreg) nreg = 16; + u32 len = 1 + (flg == 0 ? nloop * nreg : flg == 1 ? (nloop * nreg + 1) / 2 : nloop); + for (u32 k = 0; k < len && n < 0x400; k++) { memcpy(buf + n * 4, mem + ((q + k) & 0x3FF) * 4, 16); n++; } + q = (q + len) & 0x3FF; + if (eop) break; + } + kicks++; kick_qw += n; kbuf_n = n; kbuf_q = qaddr & 0x3FF; + { + static int badleft = 2; + const u32 *t0 = mem + (qaddr & 0x3FF) * 4; + if (badleft > 0 && !(t0[0] | t0[1] | t0[2] | t0[3])) { + void uya_vif1_dump_hist(void); + badleft--; + uya_log("XGKICK of an empty GIF tag at qw %03X (image %s from %04X); vi:", qaddr & 0x3FF, cur_name, cur_pc); + for (int k = 0; k < 16; k += 4) uya_log(" vi%-2d %04X %04X %04X %04X", k, rc_vu1.vi[k] & 0xFFFF, rc_vu1.vi[k + 1] & 0xFFFF, rc_vu1.vi[k + 2] & 0xFFFF, rc_vu1.vi[k + 3] & 0xFFFF); + uya_vif1_dump_hist(); + } + } + static int left = -1; /* testing: UYA_KICKLOG=n lists the first n packets */ + if (left < 0) { const char *e = getenv("UYA_KICKLOG"); left = e ? atoi(e) : 0; } + if (left > 0) { + left--; + uya_log("XGKICK at qw 0x%03X, %u qwords, image %s from 0x%04X:", qaddr & 0x3FF, n, cur_name, cur_pc); + for (u32 k = 0; k < n && k < 24; k++) uya_log(" %08X %08X %08X %08X", buf[k * 4], buf[k * 4 + 1], buf[k * 4 + 2], buf[k * 4 + 3]); + } + { /* "PATH1 @ kick " (snprintf's format, by hand: there are thousands of kicks a frame) */ + extern const char *uya_gs_src; static char src[64]; + static const char hx[] = "0123456789ABCDEF"; + char *o = src; const char *nm = cur_name; + memcpy(o, "PATH1 ", 6); o += 6; + while (*nm && o < src + 40) *o++ = *nm++; + *o++ = '@'; + for (int sh = 12; sh >= 0; sh -= 4) *o++ = hx[(cur_pc >> sh) & 15]; + memcpy(o, " kick ", 6); o += 6; + for (int sh = 8; sh >= 0; sh -= 4) *o++ = hx[((qaddr & 0x3FF) >> sh) & 15]; + *o = 0; + uya_gs_src = src; + } + uya_gif_packet(buf, n); +} + +void vu_stats(void) +{ + uya_log("VU microprograms run: VU0 %llu, VU1 %llu; XGKICK %llu (%llu qwords)", runs[0], runs[1], kicks, kick_qw); + for (int i = 0; i < vu_image_count && i < MAX_IMG; i++) { + if (!img_runs[i]) continue; + char line[256]; int o = 0; + for (int k = 0; k < 0x800 && o < 200; k++) + if (pc_runs[i] && pc_runs[i][k]) o += snprintf(line + o, sizeof line - o, " %04X:%u", k * 8, pc_runs[i][k]); + line[o] = 0; + uya_log(" image %s (VU%d): %llu runs, by start address%s", vu_images[i].name, vu_images[i].vu, img_runs[i], line); + } +} + +/* the EE's vcallms / vcallmsr */ +void rc_vcallms(RcCtx *c, u32 addr) +{ + (void)c; + rc_vu0.susp = 0; /* a new program replaces a parked one */ + vu_run(0, addr); +} + +/* The EE wrote a VI register (ctc2): a VU0 program parked in a wait loop for + * it carries on until it ends or parks again. */ +void rc_vu0_resume(void) +{ + if (!rc_vu0.susp) return; + rc_vu0.susp = 0; + vu_run(0, rc_vu0.tpc); +} diff --git a/games/rac1/host/src/gs/gs.c b/games/rac1/host/src/gs/gs.c new file mode 100644 index 0000000..c5e0b92 --- /dev/null +++ b/games/rac1/host/src/gs/gs.c @@ -0,0 +1,2384 @@ +/* gs.c: a software Graphics Synthesizer, enough for the menu. + * + * Input is GIF data (PATH2 from VIF1 DIRECT, PATH3 from GIF DMA). It keeps + * the 4 MB local memory with the real swizzled layouts, so a texture written + * in one format and read in another (fonts, CLUTs) works like on the PS2. + * + * Done: every primitive type, flat and Gouraud colour, ST/Q and UV texturing + * (32/24/16-bit, 8/4-bit with CLUT, 8H/4HL/4HH), nearest and bilinear + * filtering, mipmapping, wrap modes, texture functions, fog, alpha test, + * destination alpha test, depth test, alpha blending, colour clamp, + * FBMSK/ZMSK, scissor, host-to-local and local-to-local transfers, CLUT + * loading (CSM1), and the display circuits (PMODE/DISPFB/DISPLAY) for showing + * a frame. + * Not done: antialiasing, dithering, CSM2, local-to-host transfers. */ +#include +#include +#include +#include +#include +#include +#include "uya_host.h" +#include "uya_runtime.h" +#include "gs_swizzle.h" +#include "gs_types.h" +#include "gs_gl.h" +static inline unsigned long long rdtsc_(void) { unsigned lo, hi; __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi)); return ((unsigned long long)hi << 32) | lo; } +unsigned long long uya_t_gs, uya_tex_px; +int uya_timing = -1; /* UYA_FPSLOG: time the GS and the VUs (rdtsc around each primitive, packet and VU run) */ +static int timing(void) { if (uya_timing < 0) uya_timing = getenv("UYA_FPSLOG") != 0; return uya_timing; } + +#define BITS(v, lo, n) ((u32)(((v) >> (lo)) & ((1ull << (n)) - 1))) + +static u32 *vram; /* 1M words = 4 MB */ + +/* ---- addressing ---- */ +static inline u32 a32(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 5) * bw + (x >> 6)) * 32 + gs_block32[(y >> 3) & 3][(x >> 3) & 7]; + return ((b & 0x3FFF) << 6) | gs_col32[y & 7][x & 7]; +} +static inline u32 a32z(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 5) * bw + (x >> 6)) * 32 + (gs_block32[(y >> 3) & 3][(x >> 3) & 7] ^ 0x18); + return ((b & 0x3FFF) << 6) | gs_col32[y & 7][x & 7]; +} +static inline u32 a16(u32 bp, u32 bw, u32 x, u32 y, const uint8_t (*bt)[4], u32 z) +{ + u32 b = bp + ((y >> 6) * bw + (x >> 6)) * 32 + (bt[(y >> 3) & 7][(x >> 4) & 3] ^ z); + return ((b & 0x3FFF) << 7) | gs_col16[y & 7][x & 15]; /* halfword index */ +} +static inline u32 a8(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 6) * (bw >> 1) + (x >> 7)) * 32 + gs_block8[(y >> 4) & 3][(x >> 4) & 7]; + return ((b & 0x3FFF) << 8) | gs_col8[y & 15][x & 15]; /* byte index */ +} +static inline u32 a4(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 7) * (bw >> 1) + (x >> 7)) * 32 + gs_block4[(y >> 4) & 7][(x >> 5) & 3]; + return ((b & 0x3FFF) << 9) | gs_col4[y & 15][x & 31]; /* nibble index */ +} + +static inline u16 *vram16(void) { return (u16 *)vram; } +static inline u8 *vram8(void) { return (u8 *)vram; } + +/* raw pixel read/write in any storage format (colour formats return the stored bits) */ +static u32 px_read(u32 psm, u32 bp, u32 bw, u32 x, u32 y) +{ + switch (psm) { + case 0x00: case 0x01: return vram[a32(bp, bw, x, y)]; + case 0x02: return vram16()[a16(bp, bw, x, y, gs_block16, 0)]; + case 0x0A: return vram16()[a16(bp, bw, x, y, gs_block16s, 0)]; + case 0x13: return vram8()[a8(bp, bw, x, y)]; + case 0x14: { u32 n = a4(bp, bw, x, y); return (vram8()[n >> 1] >> ((n & 1) * 4)) & 15; } + case 0x1B: return vram[a32(bp, bw, x, y)] >> 24; + case 0x24: return (vram[a32(bp, bw, x, y)] >> 24) & 15; + case 0x2C: return vram[a32(bp, bw, x, y)] >> 28; + case 0x30: case 0x31: return vram[a32z(bp, bw, x, y)]; + case 0x32: return vram16()[a16(bp, bw, x, y, gs_block16, 0x18)]; + case 0x3A: return vram16()[a16(bp, bw, x, y, gs_block16s, 0x18)]; + } + return 0; +} +static void px_write(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 v) +{ + u32 i; + switch (psm) { + case 0x00: vram[a32(bp, bw, x, y)] = v; break; + case 0x01: i = a32(bp, bw, x, y); vram[i] = (vram[i] & 0xFF000000) | (v & 0xFFFFFF); break; + case 0x02: vram16()[a16(bp, bw, x, y, gs_block16, 0)] = (u16)v; break; + case 0x0A: vram16()[a16(bp, bw, x, y, gs_block16s, 0)] = (u16)v; break; + case 0x13: vram8()[a8(bp, bw, x, y)] = (u8)v; break; + case 0x14: i = a4(bp, bw, x, y); vram8()[i >> 1] = (u8)((vram8()[i >> 1] & ((i & 1) ? 0x0F : 0xF0)) | ((v & 15) << ((i & 1) * 4))); break; + case 0x1B: i = a32(bp, bw, x, y); vram[i] = (vram[i] & 0x00FFFFFF) | (v << 24); break; + case 0x24: i = a32(bp, bw, x, y); vram[i] = (vram[i] & 0xF0FFFFFF) | ((v & 15) << 24); break; + case 0x2C: i = a32(bp, bw, x, y); vram[i] = (vram[i] & 0x0FFFFFFF) | ((v & 15) << 28); break; + case 0x30: vram[a32z(bp, bw, x, y)] = v; break; + case 0x31: i = a32z(bp, bw, x, y); vram[i] = (vram[i] & 0xFF000000) | (v & 0xFFFFFF); break; + case 0x32: vram16()[a16(bp, bw, x, y, gs_block16, 0x18)] = (u16)v; break; + case 0x3A: vram16()[a16(bp, bw, x, y, gs_block16s, 0x18)] = (u16)v; break; + } +} +static u32 psm_bits(u32 psm) +{ + switch (psm) { + case 0x01: case 0x31: return 24; + case 0x02: case 0x0A: case 0x32: case 0x3A: return 16; + case 0x13: return 8; + case 0x14: return 4; + default: return 32; /* 32-bit, and the H formats take whole words of GS memory */ + } +} + +/* bits per pixel of transfer data (host <-> local): the H formats send 8 or + * 4 bits a pixel like PSMT8/PSMT4, though they sit in 32-bit words in GS + * memory (psm_bits above, for the memory they take) */ +static u32 trx_bits(u32 psm) +{ + switch (psm) { + case 0x1B: return 8; + case 0x24: case 0x2C: return 4; + default: return psm_bits(psm); + } +} + +/* ---- registers ---- */ + +static gs_ctx_t ctx[2]; +static u64 prim, prmode, prmodecont = 1, rgbaq, st, uv, fog, texa, fogcol, colclamp, pabe, dthe; +static u64 bitbltbuf, trxpos, trxreg, texclut; +static float qreg = 1.0f; +static u32 clut[256]; /* loaded CLUT entries (raw 32-bit or 16-bit values) */ +static u32 clut_hash; +static u32 cbp0, cbp1; + +static vtx_t vq[3]; +static int vcount; /* vertices queued for the current primitive */ + +static unsigned long long stat_prims, stat_pixels; +static int tc_nopin = -1; +static unsigned long long gs_thread_busy, n_tc_reuse, n_tc_stale, n_mark_trx, n_mark_draw, n_drain_wait, n_tc_build, n_trx_px, t_tcb, t_trx, t_draw; +extern const char *uya_gs_src; +static const char *gs_src_int = "?"; /* the source of the data the GS is working on */ +static int mtgs; /* the GS runs on its own thread */ +static const u32 *gif_cur_qw; /* testing: the GIF qword being processed */ + +static int trxlog_on(void); +/* ---- CLUT load (CSM1 layout) ---- */ +static void clut_read(u64 t0); +/* GL: exchange the loaded palette with a saved one (gs_gl.c keeps a batch's) */ +void gs_clut_swap(u32 *pal, u32 *hash) +{ + u32 t[256]; memcpy(t, clut, sizeof t); memcpy(clut, pal, sizeof t); memcpy(pal, t, sizeof t); + u32 h = clut_hash; clut_hash = *hash; *hash = h; +} +static u64 clut_pend; static int clut_pending; /* GL: a load not done yet */ +static void clut_flush(void) +{ + if (!clut_pending) return; + clut_pending = 0; + gsgl_cpu_read(BITS(clut_pend, 37, 14), 4); + clut_read(clut_pend); +} +static void clut_load(u64 t0) +{ + u32 psm = BITS(t0, 20, 6), cbp = BITS(t0, 37, 14), cpsm = BITS(t0, 51, 4), csa = BITS(t0, 56, 5); + u32 cld = BITS(t0, 61, 3); + int load = 0; + switch (cld) { + case 1: load = 1; break; + case 2: load = 1; cbp0 = cbp; break; + case 3: load = 1; cbp1 = cbp; break; + case 4: if (cbp != cbp0) { load = 1; cbp0 = cbp; } break; + case 5: if (cbp != cbp1) { load = 1; cbp1 = cbp; } break; + } + if (trxlog_on()) uya_log("TEX0 %016llX: CLD %u CBP %04X CSA %u PSM %X CPSM %X -> %s", (unsigned long long)t0, cld, cbp, csa, psm, cpsm, load ? "load" : "keep"); + if (!load) return; + if (gsgl_on) { + int pal = psm == 0x13 || psm == 0x1B || psm == 0x14 || psm == 0x24 || psm == 0x2C; + gsgl_clut_changed(); /* (a batch decoding later uses the old palette) */ + /* GL: a load from memory a GPU target owns waits until a primitive + * uses the palette: games point CBP at the frame buffer to force the + * next load (CLD 4/5), and a full 8-bit load replaces it anyway */ + /* (a pending load from a GPU target that another load replaces is + * dropped, not read back: the force-load trick's frame-buffer data is + * never used; reading it cost a GPU readback a frame in the menu) */ + clut_pending = 0; + if (!pal) return; + if (gsgl_tex_gpu(0x00, cbp, 1, 16, 16)) { clut_pend = t0; clut_pending = 1; return; } + gsgl_cpu_read(cbp, 4); + } + clut_read(t0); +} +/* the palette into clut[] (CSM1) */ +static void clut_read(u64 t0) +{ + u32 psm = BITS(t0, 20, 6), cbp = BITS(t0, 37, 14), cpsm = BITS(t0, 51, 4), csa = BITS(t0, 56, 5); + u32 cp = (cpsm == 0) ? 0x00 : (cpsm == 0xA ? 0x0A : 0x02); + if ((psm == 0x13 || psm == 0x1B) && cp == 0x00) { /* the same as below, the 32-bit reads inline */ + for (u32 i = 0; i < 256; i++) { + u32 x = (i & 7) | ((i & 0x10) >> 1), y = ((i >> 3) & 1) | ((i >> 5) << 1); + clut[i] = vram[a32(cbp, 1, x, y)]; + } + } else if (psm == 0x13 || psm == 0x1B) { /* 8-bit: 256 entries, 16x16 with index bits 3 and 4 swapped */ + for (u32 i = 0; i < 256; i++) { + u32 x = (i & 7) | ((i & 0x10) >> 1), y = ((i >> 3) & 1) | ((i >> 5) << 1); + clut[i] = px_read(cp, cbp, 1, x, y); + } + } else if (psm == 0x14 || psm == 0x24 || psm == 0x2C) { /* 4-bit: 16 entries, 8x2, at CSA */ + u32 base = (cp == 0x00 ? (csa & 15) : csa) * 16; + for (u32 i = 0; i < 16; i++) clut[(base + i) & 255] = px_read(cp, cbp, 1, i & 7, i >> 3); + } + { /* a hash of the palette, for the texture cache */ + u32 h = 2166136261u; + for (int i = 0; i < 256; i++) h = (h ^ clut[i]) * 16777619u; + clut_hash = h; + if (trxlog_on()) uya_log(" clut hash %08X", h); + } +} + +static void prim_reset(void) { vcount = 0; } +/* testing: UYA_TRXLOG=from,to logs transfers, CLUT loads and texture builds in that vsync range */ +static int trxlog_on(void) +{ + static int st = -1; static unsigned a, b; + if (st < 0) { const char *e = getenv("UYA_TRXLOG"); st = e && sscanf(e, "%u,%u", &a, &b) == 2; } + if (!st) return 0; + unsigned f = uya_frame_count(); + static unsigned lastf; + if (f >= a && f <= b && f != lastf) { lastf = f; uya_log("---- vsync %u", f); } + return f >= a && f <= b; +} + +/* ---- texture sampling ---- */ +typedef struct { u8 r, g, b, a; } rgba_t; + +static inline rgba_t c32(u32 v) { rgba_t c = {(u8)v, (u8)(v >> 8), (u8)(v >> 16), (u8)(v >> 24)}; return c; } +static inline rgba_t c16(u32 v, int aem, u8 ta0, u8 ta1) +{ + rgba_t c; + c.r = (u8)((v & 31) << 3); c.g = (u8)(((v >> 5) & 31) << 3); c.b = (u8)(((v >> 10) & 31) << 3); + c.a = (v & 0x8000) ? ta1 : ((aem && !(v & 0x7FFF)) ? 0 : ta0); + return c; +} + +/* the texture at mip level `lvl` (0: TEX0's own) */ +/* where mip level `lvl` of the context's texture is (0: TEX0's own) */ +static void tex_level(const gs_ctx_t *c, int lvl, u32 *tbp_, u32 *tbw_, u32 *tw_, u32 *th_) +{ + u64 t0 = c->tex0; + u32 tbp = BITS(t0, 0, 14), tbw = BITS(t0, 14, 6), psm = BITS(t0, 20, 6); + u32 tw = 1u << BITS(t0, 26, 4), th = 1u << BITS(t0, 30, 4); + if (lvl > 0) { + if (BITS(c->tex1, 9, 1)) { /* MTBA: levels 1-3 follow level 0 */ + u32 b = tbp, w = tw, h = th, bw = tbw; + for (int k = 0; k < lvl && k < 3; k++) { + u32 bits = psm_bits(psm); b += (w * h * bits / 8 + 255) / 256; w >>= 1; h >>= 1; bw = bw > 1 ? bw >> 1 : 1; + } + tbp = b; tbw = bw; + } else { + u64 m = lvl <= 3 ? c->miptbp1 : c->miptbp2; + int k = (lvl - 1) % 3; + tbp = BITS(m, k * 20, 14); tbw = BITS(m, k * 20 + 14, 6); + } + tw >>= lvl; th >>= lvl; + if (!tw) tw = 1; + if (!th) th = 1; + } + if (tw > 1024) tw = 1024; + if (th > 1024) th = 1024; + *tbp_ = tbp; *tbw_ = tbw; *tw_ = tw; *th_ = th; +} + +/* one texel, decoded to RGBA (CLUT and TEXA applied) */ +static inline u32 rgba_u32(rgba_t r) { return (u32)r.r | ((u32)r.g << 8) | ((u32)r.b << 16) | ((u32)r.a << 24); } +static inline rgba_t u32_rgba(u32 v) { rgba_t r = { (u8)v, (u8)(v >> 8), (u8)(v >> 16), (u8)(v >> 24) }; return r; } +static rgba_t texel_raw(u64 t0, u32 tbp, u32 tbw, u32 x, u32 y) +{ + u32 psm = BITS(t0, 20, 6); + u8 ta0 = (u8)BITS(texa, 0, 8), ta1 = (u8)BITS(texa, 32, 8); + int aem = (int)BITS(texa, 15, 1); + u32 cpsm = BITS(t0, 51, 4), csa = BITS(t0, 56, 5); + u32 v = px_read(psm, tbp, tbw, x & 2047, y & 2047); + rgba_t r; + switch (psm) { + case 0x00: return c32(v); + case 0x01: r = c32(v); r.a = (aem && !(v & 0xFFFFFF)) ? 0 : ta0; return r; + case 0x02: case 0x0A: return c16(v, aem, ta0, ta1); + case 0x13: case 0x1B: v = clut[v & 255]; break; + case 0x14: case 0x24: case 0x2C: v = clut[((cpsm == 0 ? (csa & 15) : csa) * 16 + v) & 255]; break; + default: return c32(v); + } + return cpsm == 0 ? c32(v) : c16(v, aem, ta0, ta1); +} + +/* ---- texture cache ---- + * Textures are decoded (swizzle, CLUT, TEXA) into plain RGBA arrays once and + * reused until GS memory under them changes: transfers and draws mark the + * 8 KB pages they write (page_gen), and an entry is checked against its pages + * once per primitive. */ +static u32 page_gen[512], dirty_epoch = 1; +static void mark_blocks(u32 bp, u32 nblocks) +{ + u32 p0 = bp / 32, p1 = (bp + (nblocks ? nblocks : 1) - 1) / 32; + if (p1 > 511) p1 = 511; + for (u32 p = p0; p <= p1 && p < 512; p++) page_gen[p]++; + dirty_epoch++; +} +/* Content of GS memory by page, for the texture cache: games stream textures + * through the same memory every frame (Kerwan: ~370 rebuilds a vsync), so an + * entry whose pages were written again is still good if the bytes are the + * same. A page is hashed once per change (page_gen). */ +static u32 page_hg[512] = { [0 ... 511] = ~0u }; +static u64 page_h[512]; +static u64 pages_content(u32 p0, u32 p1) +{ + u64 h = 0x9E3779B97F4A7C15ull ^ ((u64)p0 << 32) ^ p1; + for (u32 p = p0; p <= p1; p++) { + if (page_hg[p] != page_gen[p]) { + const u64 *w = (const u64 *)(vram + p * 2048); + u64 a = 0, b = 0, c = 0, d = 0; + for (u32 i = 0; i < 1024; i += 4) { + a = (a ^ w[i]) * 0x100000001B3ull; b = (b ^ w[i + 1]) * 0x9E3779B97F4A7C15ull; + c = (c ^ w[i + 2]) * 0xC2B2AE3D27D4EB4Full; d = (d ^ w[i + 3]) * 0x165667B19E3779F9ull; + } + page_h[p] = a ^ (b << 1) ^ (c << 2) ^ (d << 3) ^ (a >> 29) ^ (c >> 31); + page_hg[p] = page_gen[p]; + } + h = (h ^ page_h[p]) * 0x100000001B3ull + p; + } + return h; +} +/* blocks a w x h area of a buffer (base bp, width bw in 64-pixel units) can touch */ +static u32 area_blocks(u32 psm, u32 bw, u32 h) +{ + u32 bits = psm_bits(psm); + if (bits == 24) bits = 32; + u32 w = (bw ? bw : 1) * 64; + return (w * h * bits / 8 + 255) / 256 + 64; +} +typedef struct { u64 k1, k2, ch; u32 epoch, sum, p0, p1, w, h, used, ser; u32 *px; } tcache_t; +#define TC_BUDGET ((size_t)256 << 20) /* bytes of decoded textures kept */ +static size_t tc_bytes; +static tcache_t tc[TC_N]; +static u32 tc_clock, prim_serial; +static struct { u32 serial; const tcache_t *e; } tc_memo[8]; +/* a whole texture level to RGBA, a swizzle block at a time */ +static void decode_tex(u32 *out, u64 t0, u32 tbp, u32 tbw, u32 tw, u32 th) +{ + u32 psm = BITS(t0, 20, 6), cpsm = BITS(t0, 51, 4), csa = BITS(t0, 56, 5); + u8 ta0 = (u8)BITS(texa, 0, 8), ta1 = (u8)BITS(texa, 32, 8); + int aem = (int)BITS(texa, 15, 1); + u32 pal[256]; + if (psm == 0x13 || psm == 0x1B || psm == 0x14 || psm == 0x24 || psm == 0x2C) { + u32 off = (psm == 0x13 || psm == 0x1B) ? 0 : (cpsm == 0 ? (csa & 15) : csa) * 16; + u32 n = (psm == 0x13 || psm == 0x1B) ? 256 : 16; + for (u32 i = 0; i < n; i++) { u32 v = clut[(off + i) & 255]; pal[i] = rgba_u32(cpsm == 0 ? c32(v) : c16(v, aem, ta0, ta1)); } + } + for (u32 y = 0; y < th; y++) { + u32 *row = out + y * tw; + u32 yy = y & 2047; + switch (psm) { + case 0x13: { + const u8 *col = gs_col8[yy & 15]; + for (u32 x0 = 0; x0 < tw; x0 += 16) { + u32 base = a8(tbp, tbw, x0, yy) - col[0], n = tw - x0 < 16 ? tw - x0 : 16; + for (u32 i = 0; i < n; i++) row[x0 + i] = pal[vram8()[base + col[i]]]; + } + } break; + case 0x14: { + const u16 *col = gs_col4[yy & 15]; + for (u32 x0 = 0; x0 < tw; x0 += 32) { + u32 base = a4(tbp, tbw, x0, yy) - col[0], n = tw - x0 < 32 ? tw - x0 : 32; + for (u32 i = 0; i < n; i++) { u32 nb = base + col[i]; row[x0 + i] = pal[(vram8()[nb >> 1] >> ((nb & 1) * 4)) & 15]; } + } + } break; + case 0x00: case 0x01: case 0x1B: case 0x24: case 0x2C: { + const u8 *col = gs_col32[yy & 7]; + for (u32 x0 = 0; x0 < tw; x0 += 8) { + u32 base = a32(tbp, tbw, x0, yy) - col[0], n = tw - x0 < 8 ? tw - x0 : 8; + for (u32 i = 0; i < n; i++) { + u32 v = vram[base + col[i]]; + switch (psm) { + case 0x00: row[x0 + i] = v; break; + case 0x01: { rgba_t r = c32(v); r.a = (aem && !(v & 0xFFFFFF)) ? 0 : ta0; row[x0 + i] = rgba_u32(r); } break; + case 0x1B: row[x0 + i] = pal[v >> 24]; break; + case 0x24: row[x0 + i] = pal[(v >> 24) & 15]; break; + default: row[x0 + i] = pal[v >> 28]; break; + } + } + } + } break; + case 0x02: case 0x0A: { + const uint8_t (*bt)[4] = psm == 0x02 ? gs_block16 : gs_block16s; + const u8 *col = gs_col16[yy & 7]; + for (u32 x0 = 0; x0 < tw; x0 += 16) { + u32 base = a16(tbp, tbw, x0, yy, bt, 0) - col[0], n = tw - x0 < 16 ? tw - x0 : 16; + for (u32 i = 0; i < n; i++) row[x0 + i] = rgba_u32(c16(vram16()[base + col[i]], aem, ta0, ta1)); + } + } break; + default: + for (u32 x = 0; x < tw; x++) row[x] = rgba_u32(texel_raw(t0, tbp, tbw, x, yy)); + break; + } + } +} + +static void gs_drain(void); +static int dbg_race(u32 d0, u32 d1, const char *what, u32 bp); +static int dbg_drain_all = -1; +static u32 n_layout_drain; /* testing: UYA_DRAINALL=1 (texture builds), 2 (transfers), 3 (both) */ +static u8 page_drawn[512]; /* pages draws (maybe still queued) write */ +static int range_drawn(u32 bp, u32 n) +{ + u32 p0 = bp / 32, p1 = (bp + (n ? n : 1) - 1) / 32; + if (p1 > 511) p1 = 511; + for (u32 p = p0; p <= p1; p++) if (page_drawn[p]) return 1; + return 0; +} +static u32 cur_target[6]; /* the current target's pages (frame, then Z), still being drawn; [4], [5]: their layouts */ +/* A page's layout (buffer width and format) as queued draws use it: workers + * own rows, and the same memory seen with another width or format puts a + * row in other rows, so a draw with another layout over queued draws' pages + * must wait for them (else a worker can read what another has not written) */ +static u16 page_layout[512]; +static int layout_conflict(u32 bp, u32 n, u16 lay) +{ + u32 p0 = bp / 32, p1 = (bp + (n ? n : 1) - 1) / 32; + if (p1 > 511) p1 = 511; + for (u32 p = p0; p <= p1; p++) if (page_drawn[p] && page_layout[p] != lay) return 1; + return 0; +} +static void set_drawn(u32 bp, u32 n, u16 lay) +{ + u32 p0 = bp / 32, p1 = (bp + (n ? n : 1) - 1) / 32; + if (p1 > 511) p1 = 511; + for (u32 p = p0; p <= p1; p++) { page_drawn[p] = 1; page_layout[p] = lay; } +} +static void set_drawn_target(void) { set_drawn(cur_target[0], cur_target[1], (u16)cur_target[4]); if (cur_target[3]) set_drawn(cur_target[2], cur_target[3], (u16)cur_target[5]); } +static void st_dirty_force(void); +static void retire(void *px); +static size_t retire_n; +static void retire_free(int all); +/* Decoded textures come and go all the time (about 200 a vsync in a level, as + * the game streams textures through GS memory). Big blocks from malloc are + * fresh mappings that fault in page by page, so freed buffers are kept for + * reuse by size (a few dozen, at most 64 MB). */ +static struct { void *p; size_t n; } tpool[64]; +static int tpool_n; static size_t tpool_bytes; +static void *tex_alloc(size_t n) +{ + for (int i = tpool_n - 1; i >= 0; i--) + if (tpool[i].n == n) { void *p = tpool[i].p; tpool_bytes -= n; tpool[i] = tpool[--tpool_n]; return p; } + return malloc(n); +} +static void tex_free(void *p, size_t n) +{ + if (!p) return; + if (n >= 4096 && tpool_bytes + n <= ((size_t)64 << 20)) { + if (tpool_n == 64) { free(tpool[0].p); tpool_bytes -= tpool[0].n; tpool[0] = tpool[--tpool_n]; } + tpool[tpool_n].p = p; tpool[tpool_n].n = n; tpool_n++; tpool_bytes += n; + return; + } + free(p); +} +static void tex_pool_flush(void) { while (tpool_n) { tpool_n--; free(tpool[tpool_n].p); } tpool_bytes = 0; } +static const tcache_t *tc_get(const gs_ctx_t *c, int lvl) +{ + if (tc_memo[lvl].serial == prim_serial) return tc_memo[lvl].e; + u64 t0 = c->tex0; + u32 tbp, tbw, tw, th, psm = BITS(t0, 20, 6); + tex_level(c, lvl, &tbp, &tbw, &tw, &th); + if (lvl == 0) { /* the REGION modes can address past the texture's size: decode that far too */ + u32 wms = BITS(c->clamp, 0, 2), wmt = BITS(c->clamp, 2, 2); + u32 mu = BITS(c->clamp, 14, 10), mv = BITS(c->clamp, 34, 10); + if (wms == 2 && mu + 1 > tw) tw = mu + 1; + if (wms == 3 && (mu | BITS(c->clamp, 4, 10)) + 1 > tw) tw = (mu | BITS(c->clamp, 4, 10)) + 1; + if (wmt == 2 && mv + 1 > th) th = mv + 1; + if (wmt == 3 && (mv | BITS(c->clamp, 24, 10)) + 1 > th) th = (mv | BITS(c->clamp, 24, 10)) + 1; + if (tw > 1024) tw = 1024; + if (th > 1024) th = 1024; + } + u64 k1 = (u64)tbp | ((u64)tbw << 14) | ((u64)psm << 20) | ((u64)tw << 26) | ((u64)th << 40); + u32 pal = (psm == 0x13 || psm == 0x1B || psm == 0x14 || psm == 0x24 || psm == 0x2C); + u64 k2 = pal ? ((u64)clut_hash | ((u64)BITS(t0, 51, 4) << 32) | ((u64)BITS(t0, 56, 5) << 36)) : 0; + k2 ^= (u64)(BITS(texa, 0, 8) | (BITS(texa, 15, 1) << 8) | (BITS(texa, 32, 8) << 9)) << 44; + static tcache_t *last[8]; + tcache_t *e = 0; + static int16_t hidx[8192]; /* hash of the keys -> entry + 1 */ + u32 hh = (u32)((k1 * 0x9E3779B97F4A7C15ull ^ k2 * 0xC2B2AE3D27D4EB4Full) >> 51); + u32 p0 = tbp / 32, p1 = (tbp + area_blocks(psm, tbw, th)) / 32; + if (p1 > 511) p1 = 511; + u32 sum = 0; int have_sum = 0; + u64 ch = 0; int have_ch = 0; + if (last[lvl] && last[lvl]->px && last[lvl]->used && last[lvl]->k1 == k1 && last[lvl]->k2 == k2 && last[lvl]->epoch == dirty_epoch) e = last[lvl]; + else { + /* the entries of these keys (several, one per content the memory held): the + * one whose pages did not change, else one with the same bytes there now */ + int stale_seen = 0; + for (u32 k = 0; k < 16 && !e; k++) { + int j = hidx[(hh + k) & 8191] - 1; + if (j < 0 || !tc[j].px || !tc[j].used || tc[j].k1 != k1 || tc[j].k2 != k2) continue; + tcache_t *x = &tc[j]; + if (x->epoch == dirty_epoch) { e = x; break; } + if (!have_sum) { for (u32 p = p0; p <= p1; p++) sum += page_gen[p]; have_sum = 1; } + if (x->sum == sum) { x->epoch = dirty_epoch; e = x; break; } + /* the bytes decide, unless a GPU target owns some of those pages (GL: GS memory is not current there) */ + if ((dbg_drain_all & 128) || (gsgl_on && gsgl_tex_owned(psm, tbp, tbw, tw, th))) { x->used = 0; x->k1 = ~0ull; stale_seen = 1; continue; } + if (range_drawn(p0 * 32, (p1 - p0 + 1) * 32)) gs_drain(); /* queued draws may still write those pages */ + if (!have_ch) { ch = pages_content(p0, p1); have_ch = 1; } + if (x->ch == ch) { x->sum = sum; x->epoch = dirty_epoch; e = x; n_tc_reuse++; break; } + stale_seen = 1; + } + if (!e && stale_seen) n_tc_stale++; + } + if (dbg_drain_all & 64) e = 0; + if (!e) { + /* the texture's memory must hold what the queued draws put there; the + * entry's old pixels may still be in use by queued draws, so they are + * retired (freed once the workers are past them), not overwritten */ + if (dbg_drain_all & 1 || range_drawn(p0 * 32, (p1 - p0 + 1) * 32)) gs_drain(); + else if (dbg_drain_all & 8) dbg_race(p0, p1, "texture", tbp); + n_tc_build++; + if (trxlog_on()) uya_log("tex build lvl %d TBP %04X TBW %u PSM %X %ux%u k2 %llX pages %u-%u", lvl, tbp, tbw, psm, tw, th, (unsigned long long)k2, p0, p1); + int best = 0; /* the least recently used slot */ + extern u32 gsgl_tc_pin[TC_N], gsgl_batch_id; + if (tc_nopin < 0) tc_nopin = getenv("UYA_TC_NOPIN") != 0; /* testing: the old behaviour */ +#define TC_PINNED(i) (gsgl_on && gsgl_tc_pin[i] == gsgl_batch_id && !tc_nopin) /* the GL batch being built uses it */ + while (TC_PINNED(best) && best < TC_N - 1) best++; + for (int i = 0; i < TC_N; i++) { if (TC_PINNED(i)) continue; if (!tc[i].px || !tc[i].used) { best = i; break; } if (tc[i].used < tc[best].used) best = i; } + e = &tc[best]; + { /* put it in the hash (a free or stale slot of its probe run) */ + u32 k; + for (k = 0; k < 16; k++) { int j = hidx[(hh + k) & 8191] - 1; if (j < 0 || j == best || !tc[j].px || !tc[j].used) break; } + hidx[(hh + (k < 16 ? k : 0)) & 8191] = (int16_t)(best + 1); + } + if (e->px) { retire_n = (size_t)e->w * e->h * 4; retire(e->px); tc_bytes -= (size_t)e->w * e->h * 4; e->px = 0; } + { /* a memory budget for the decoded textures: the least recently used go first */ + size_t need = (size_t)tw * th * 4; + static size_t budget; /* testing: UYA_TC_BUDGET_MB=n keeps less (evictions often) */ + if (!budget) { const char *x = getenv("UYA_TC_BUDGET_MB"); budget = x && atoi(x) > 0 ? (size_t)atoi(x) << 20 : TC_BUDGET; } + while (tc_bytes + need > budget) { + int lru = -1; + for (int i = 0; i < TC_N; i++) + if (tc[i].px && &tc[i] != e && !TC_PINNED(i) && tc[i].used + 8 < tc_clock && (lru < 0 || tc[i].used < tc[lru].used)) lru = i; + if (lru < 0) break; + retire_n = (size_t)tc[lru].w * tc[lru].h * 4; retire(tc[lru].px); tc_bytes -= (size_t)tc[lru].w * tc[lru].h * 4; + tc[lru].px = 0; tc[lru].used = 0; tc[lru].k1 = ~0ull; + } + e->px = tex_alloc(need); + if (!e->px) { gs_drain(); retire_free(1); tex_pool_flush(); e->px = malloc(need); } + if (!e->px) { uya_log("GS: out of memory for a %ux%u texture", tw, th); exit(1); } + tc_bytes += need; + } + e->k1 = k1; e->k2 = k2; e->w = tw; e->h = th; e->p0 = p0; e->p1 = p1; + { static u32 ser_n; e->ser = ++ser_n; } + if (gsgl_on) gsgl_tex_read(psm, tbp, tbw, tw, th); /* GS memory a GPU target drew */ + unsigned long long t_ = rdtsc_(); + decode_tex(e->px, t0, tbp, tbw, tw, th); + { /* testing: UYA_TEXDUMP=tbp saves that texture (RGBA, alpha as a second image) when decoded */ + static int want = -2; static int nd; + if (want == -2) { const char *x = getenv("UYA_TEXDUMP"); want = x ? (int)strtol(x, 0, 16) : -1; } + if ((int)tbp == want && nd < 40 && (!getenv("UYA_TRXLOG") || trxlog_on())) { + char path[64]; snprintf(path, sizeof path, "tex_%04X_%02d_v%u_l%d.ppm", tbp, nd++, uya_frame_count(), lvl); + FILE *f = fopen(path, "wb"); + if (f) { fprintf(f, "P6 %u %u 255\n", tw, th * 2); + for (u32 i = 0; i < tw * th; i++) { fputc(e->px[i] & 255, f); fputc((e->px[i] >> 8) & 255, f); fputc((e->px[i] >> 16) & 255, f); } + for (u32 i = 0; i < tw * th; i++) { u32 a = e->px[i] >> 24; fputc(a > 255 ? 255 : a, f); fputc(a, f); fputc(a, f); } + fclose(f); } + uya_log("texture %04X (%ux%u, PSM %X, CBP %X) saved to %s", tbp, tw, th, (u32)BITS(t0, 20, 6), (u32)BITS(t0, 37, 14), path); + } + } + t_tcb += rdtsc_() - t_; + sum = 0; + for (u32 p = p0; p <= p1; p++) sum += page_gen[p]; + e->sum = sum; e->epoch = dirty_epoch; + e->ch = (gsgl_on && gsgl_tex_owned(psm, tbp, tbw, tw, th)) ? 0 : pages_content(p0, p1); /* (never a hash of pages a target owns) */ + } + e->used = ++tc_clock; + last[lvl] = e; + tc_memo[lvl].serial = prim_serial; tc_memo[lvl].e = e; + return e; +} + +/* ---- drawing ---- + * A primitive is turned into a command (pcmd_t: the pixel pipeline's state, + * the texture's decoded levels from the cache, the vertices) that the + * rasterizer runs. With UYA_GS_THREADS (default: the number of cores - 1, at + * most 8) worker threads run the commands, each one drawing its own rows + * (4-row bands), so every pixel still sees the primitives in order. Anything + * that reads or writes GS memory outside the drawing (transfers, the texture + * cache, the display) waits for the workers first (gs_drain). */ +/* (tlevel_t, pcmd_t, pprim_t: gs_types.h) */ + +#define AINL static inline __attribute__((always_inline)) +static inline int clamp255(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } + +static inline float fast_log2(float x) /* log2|x| to about 0.0002 */ +{ + u32 b; memcpy(&b, &x, 4); + b &= 0x7FFFFFFFu; + if (!b) return -126.0f; + float m = (float)(b & 0x7FFFFF) * (1.0f / 8388608.0f); + return (float)((int)(b >> 23) - 127) + m * (1.4385482f + m * (-0.6780911f + m * (0.3236495f - 0.0842966f * m))); +} + +static u32 texel_outside(const pcmd_t *c, int lvl, int x, int y) +{ + gs_ctx_t t; memset(&t, 0, sizeof t); + t.tex0 = c->tex0; t.tex1 = c->tex1; t.miptbp1 = c->miptbp1; t.miptbp2 = c->miptbp2; + u32 tbp, tbw, w, h; + tex_level(&t, lvl, &tbp, &tbw, &w, &h); + return rgba_u32(texel_raw(c->tex0, tbp, tbw, (u32)x, (u32)y)); +} + +AINL int wrap1(int x, int n, int wm, int mn, int mx) +{ + switch (wm) { + case 0: return x & (n - 1); + case 1: return x < 0 ? 0 : (x >= n ? n - 1 : x); + case 2: return x < mn ? mn : (x > mx ? mx : x); + default: return (x & mn) | mx; + } +} +AINL u32 tfetch(const pcmd_t *c, int lvl, int x, int y) +{ + const tlevel_t *L = &c->lv[lvl]; + int tw = L->w, th = L->h; + x = wrap1(x, L->lw, c->wms, c->minu, c->maxu); + y = wrap1(y, L->lh, c->wmt, c->minv, c->maxv); + if ((u32)x < (u32)tw && (u32)y < (u32)th) return L->px[y * tw + x]; + return texel_outside(c, lvl, x, y); +} + +AINL u32 lerp4(u32 p00, u32 p10, u32 p01, u32 p11, u32 fu, u32 fv) +{ + /* two channels at a time: red/blue and green/alpha, 8.8 fixed point */ + u32 iu = 256 - fu, iv = 256 - fv; + u32 rb0 = ((p00 & 0x00FF00FF) * iu + (p10 & 0x00FF00FF) * fu) >> 8 & 0x00FF00FF; + u32 rb1 = ((p01 & 0x00FF00FF) * iu + (p11 & 0x00FF00FF) * fu) >> 8 & 0x00FF00FF; + u32 ga0 = (((p00 >> 8) & 0x00FF00FF) * iu + ((p10 >> 8) & 0x00FF00FF) * fu) >> 8 & 0x00FF00FF; + u32 ga1 = (((p01 >> 8) & 0x00FF00FF) * iu + ((p11 >> 8) & 0x00FF00FF) * fu) >> 8 & 0x00FF00FF; + u32 rb = (rb0 * iv + rb1 * fv) >> 8 & 0x00FF00FF; + u32 ga = (ga0 * iv + ga1 * fv) >> 8 & 0x00FF00FF; + return rb | (ga << 8); +} + +AINL u32 tsample_lvl(const pcmd_t *c, int lvl, float u, float v, int linear) +{ + if (!linear) return tfetch(c, lvl, (int)floorf(u), (int)floorf(v)); + u -= 0.5f; v -= 0.5f; + float fx = floorf(u), fy = floorf(v); + int x0 = (int)fx, y0 = (int)fy; + /* u - floor(u) is in [0, 1): a signed conversion gives the same (and a NaN the same 0x80000000 as clang's unsigned one) */ + u32 fu = (u32)(s32)((u - fx) * 256.0f), fv = (u32)(s32)((v - fy) * 256.0f); + const tlevel_t *L = &c->lv[lvl]; + int tw = L->w, th = L->h; + int xa = wrap1(x0, L->lw, c->wms, c->minu, c->maxu), xb = wrap1(x0 + 1, L->lw, c->wms, c->minu, c->maxu); + int ya = wrap1(y0, L->lh, c->wmt, c->minv, c->maxv), yb = wrap1(y0 + 1, L->lh, c->wmt, c->minv, c->maxv); + u32 p00, p10, p01, p11; + if ((u32)xa < (u32)tw && (u32)xb < (u32)tw && (u32)ya < (u32)th && (u32)yb < (u32)th) { + const u32 *ra = L->px + ya * tw, *rb = L->px + yb * tw; + p00 = ra[xa]; p10 = ra[xb]; p01 = rb[xa]; p11 = rb[xb]; + } else { + p00 = tfetch(c, lvl, x0, y0); p10 = tfetch(c, lvl, x0 + 1, y0); p01 = tfetch(c, lvl, x0, y0 + 1); p11 = tfetch(c, lvl, x0 + 1, y0 + 1); + } + return lerp4(p00, p10, p01, p11, fu, fv); +} + +/* filtering and mipmapping (TEX1: LCM, MXL, MMAG, MMIN, L, K); q is the pixel's Q */ +AINL u32 tsample(const pcmd_t *c, float u, float v, float q) +{ + int lin = (c->mmin == 1 || c->mmin == 4 || c->mmin == 5); /* minification filter within a level */ + if ((c->mxl == 0 || c->mmin < 2) && lin == c->mmag) return tsample_lvl(c, 0, u, v, lin); + float lod = c->lcm ? c->k : -fast_log2(q) * (float)(1 << c->l) + c->k; + /* The GS works the LOD out to 4 fraction bits, rounded (the precision of K): + * a level boundary has that much slack. Games rely on it: RAC1 uploads only + * the levels its distance test says a moby needs (the others' TBP is stale or + * 0, the frame buffer), and its test is about as tight. */ + lod = floorf(lod * 16.0f + 0.5f) * (1.0f / 16.0f); + if (lod <= 0.0f || c->mxl == 0 || c->mmin < 2) return tsample_lvl(c, 0, u, v, lod <= 0.0f ? c->mmag : lin); + if (c->mmin == 3 || c->mmin == 5) { /* between two levels */ + int l0 = (int)floorf(lod); float fr = lod - (float)l0; + /* (x * 2^-n is x / 2^n exactly) */ + if (l0 >= (int)c->mxl) { float s = 1.0f / (float)(1 << c->mxl); return tsample_lvl(c, c->mxl, u * s, v * s, lin); } + float s0 = 1.0f / (float)(1 << l0), s1 = 1.0f / (float)(2 << l0); + u32 a = tsample_lvl(c, l0, u * s0, v * s0, lin); + u32 b = tsample_lvl(c, l0 + 1, u * s1, v * s1, lin); + u32 f = (u32)(fr * 256), o = 0; + for (int sh = 0; sh < 32; sh += 8) o |= ((((a >> sh) & 255) * (256 - f) + ((b >> sh) & 255) * f) >> 8) << sh; + return o; + } + int lvl = (int)floorf(lod + 0.5f); + if (lvl > (int)c->mxl) lvl = (int)c->mxl; + if (!lvl) return tsample_lvl(c, 0, u, v, lin); + float s = 1.0f / (float)(1 << lvl); + return tsample_lvl(c, lvl, u * s, v * s, lin); +} + +/* one pixel through the pipeline: u, v are level-0 texel coordinates */ +static int ptrace_on, ptrace_x, ptrace_y; static u32 ptrace_fbp; static const void *ptrace_prim; /* testing: UYA_PIXTRACE=x,y,fbp */ +static unsigned ptrace_from; /* optional 4th field of UYA_PIXTRACE: first vsync to log */ +static void ptrace_log(const pcmd_t *p, u32 z, int r, int g, int b, int a, float u, float v, float q) +{ + static int left = 400; + if (uya_frame_count() < ptrace_from) return; + if (left-- <= 0) return; + if (left == 399) uya_log("pixtrace: from vsync %u", uya_frame_count()); + uya_log("pixtrace [%s]: z %08X rgba %d,%d,%d,%d uv %.2f,%.2f q %g | TME %u FST %u TFX %u TCC %u tex0 %016llX tw %d th %d ABE %u (%u%u%u%u fix %u) ATE %u/%u/%u ZTE %u/%u ZPSM %X FBMSK %08X IIP %u FGE %u", + gs_src_int, z, r, g, b, a, u, v, q, p->tme, p->fst, p->tfx, p->tcc, (unsigned long long)p->tex0, p->lv[0].w, p->lv[0].h, p->abe, p->ba, p->bb, p->bc, p->bd, p->fix, + p->ate, p->atst, p->aref, p->zte, p->ztst, p->zpsm, p->fbmsk, p->iip, p->fge); + uya_log(" afail %u, fba %u, colclamp %u, prim at %p", p->afail, p->fba, p->clampc, ptrace_prim); + if (p->tme) uya_log(" tex1 %016llX miptbp1 %016llX mmin %u mmag %u mxl %u l %u k %g lcm %u: lod %g", (unsigned long long)p->tex1, (unsigned long long)p->miptbp1, + p->mmin, p->mmag, p->mxl, p->l, p->k, p->lcm, p->lcm ? p->k : -fast_log2(q) * (float)(1 << p->l) + p->k); + if (ptrace_prim) { + const pprim_t *pr = ptrace_prim; + uya_log(" prim type %u: (%.1f,%.1f rgba %u,%u,%u,%u st %g,%g q %g) (%.1f,%.1f rgba %u,%u,%u,%u) (%.1f,%.1f rgba %u,%u,%u,%u)", pr->type, + pr->v[0].x, pr->v[0].y, pr->v[0].r, pr->v[0].g, pr->v[0].b, pr->v[0].a, pr->v[0].s, pr->v[0].t, pr->v[0].q, + pr->v[1].x, pr->v[1].y, pr->v[1].r, pr->v[1].g, pr->v[1].b, pr->v[1].a, pr->v[2].x, pr->v[2].y, pr->v[2].r, pr->v[2].g, pr->v[2].b, pr->v[2].a); + } +} +static inline void pixel(const pcmd_t *p, int x, int y, u32 z, int r, int g, int b, int a, float u, float v, float q, int f) +{ + if (__builtin_expect(ptrace_on, 0) && x == ptrace_x && y == ptrace_y && p->fbp == ptrace_fbp) ptrace_log(p, z, r, g, b, a, u, v, q); + if (p->date) { /* destination alpha test (RAC1 darkens the pixels its shadows marked with it) */ + u32 m; + if (p->fpsm == 0x00) m = vram[a32(p->fbp, p->fbw, (u32)x, (u32)y)] >> 31; + else if (p->fpsm == 0x02 || p->fpsm == 0x0A) m = (px_read(p->fpsm, p->fbp, p->fbw, (u32)x, (u32)y) >> 15) & 1; + else m = p->datm; /* PSMCT24 has no alpha to test */ + if (m != p->datm) return; + } + u32 zaddr = 0; /* depth test first: a failing pixel is dropped whatever else happens */ + if (p->zpsm == 0x31 || p->zpsm == 0x30) zaddr = a32z(p->zbp, p->fbw, (u32)x, (u32)y); + if (p->ztst != 1) { + if (p->ztst == 0) return; + u32 zd = (p->zpsm == 0x31 || p->zpsm == 0x30) ? vram[zaddr] : px_read(p->zpsm, p->zbp, p->fbw, (u32)x, (u32)y); + u32 zs = z; + if (p->zpsm == 0x31) { zs &= 0xFFFFFF; zd &= 0xFFFFFF; } + if (p->zpsm == 0x32 || p->zpsm == 0x3A) { zs = zs > 0xFFFF ? 0xFFFF : zs; } + if (p->ztst == 2 ? zs < zd : zs <= zd) return; + } + if (p->tme) { + u32 t = tsample(p, u, v, q); + int tr = t & 255, tg = (t >> 8) & 255, tb = (t >> 16) & 255, ta = t >> 24; + switch (p->tfx) { + case 0: r = (tr * r) >> 7; g = (tg * g) >> 7; b = (tb * b) >> 7; a = p->tcc ? (ta * a) >> 7 : a; break; + case 1: r = tr; g = tg; b = tb; a = p->tcc ? ta : a; break; + case 2: r = ((tr * r) >> 7) + a; g = ((tg * g) >> 7) + a; b = ((tb * b) >> 7) + a; a = p->tcc ? ta + a : a; break; + default: r = ((tr * r) >> 7) + a; g = ((tg * g) >> 7) + a; b = ((tb * b) >> 7) + a; a = p->tcc ? ta : a; break; + } + r = clamp255(r); g = clamp255(g); b = clamp255(b); a = clamp255(a); + } + if (p->fge) { + r = (r * f + p->fogr * (255 - f)) >> 8; g = (g * f + p->fogg * (255 - f)) >> 8; b = (b * f + p->fogb * (255 - f)) >> 8; + } + int write_fb = 1, write_z = !p->zmsk, write_a = 1; + if (p->ate) { + int pass; + switch (p->atst) { + case 0: pass = 0; break; + case 1: pass = 1; break; + case 2: pass = a < (int)p->aref; break; + case 3: pass = a <= (int)p->aref; break; + case 4: pass = a == (int)p->aref; break; + case 5: pass = a >= (int)p->aref; break; + case 6: pass = a > (int)p->aref; break; + default: pass = a != (int)p->aref; break; + } + if (!pass) { + switch (p->afail) { + case 0: return; + case 1: write_z = 0; break; + case 2: write_fb = 0; break; + case 3: write_z = 0; write_a = 0; break; + } + } + } + if (write_fb) { + u32 old = 0, out; + int dr = 0, dg = 0, db = 0, da = 0; + int need_old = p->abe || p->fbmsk || !write_a || p->fpsm == 0x01; + u32 faddr = p->fpsm <= 1 ? a32(p->fbp, p->fbw, (u32)x, (u32)y) : 0; + if (need_old) { + old = p->fpsm <= 1 ? vram[faddr] : px_read(p->fpsm, p->fbp, p->fbw, (u32)x, (u32)y); + if (p->fpsm == 0x02 || p->fpsm == 0x0A) { + dr = (old & 31) << 3; dg = ((old >> 5) & 31) << 3; db = ((old >> 10) & 31) << 3; da = (old & 0x8000) ? 0x80 : 0; + } else { + dr = old & 255; dg = (old >> 8) & 255; db = (old >> 16) & 255; da = p->fpsm == 0x01 ? 0x80 : (int)(old >> 24); + } + } + if (p->abe) { + int cs[3] = {r, g, b}, cd[3] = {dr, dg, db}, o[3]; + int cc = p->bc == 0 ? a : (p->bc == 1 ? da : (int)p->fix); + for (int k = 0; k < 3; k++) { + int A = p->ba == 0 ? cs[k] : (p->ba == 1 ? cd[k] : 0); + int B = p->bb == 0 ? cs[k] : (p->bb == 1 ? cd[k] : 0); + int D = p->bd == 0 ? cs[k] : (p->bd == 1 ? cd[k] : 0); + o[k] = (((A - B) * cc) >> 7) + D; + } + r = o[0]; g = o[1]; b = o[2]; + } + if (p->clampc) { r = clamp255(r); g = clamp255(g); b = clamp255(b); } + else { r &= 255; g &= 255; b &= 255; } + if (p->fba) a |= 0x80; + if (!write_a) a = da; + if (p->fpsm == 0x02 || p->fpsm == 0x0A) { + out = (u32)(r >> 3) | ((u32)(g >> 3) << 5) | ((u32)(b >> 3) << 10) | ((a & 0x80) ? 0x8000u : 0); + out = (out & ~(p->fbmsk & 0xFFFF)) | (old & p->fbmsk & 0xFFFF); + } else { + out = (u32)r | ((u32)g << 8) | ((u32)b << 16) | ((u32)(a & 255) << 24); + out = (out & ~p->fbmsk) | (old & p->fbmsk); + } + if (p->fpsm == 0x00) vram[faddr] = out; + else if (p->fpsm == 0x01) vram[faddr] = (vram[faddr] & 0xFF000000u) | (out & 0xFFFFFF); + else px_write(p->fpsm, p->fbp, p->fbw, (u32)x, (u32)y, out); + } + if (write_z && p->zte) { + if (p->zpsm == 0x30) vram[zaddr] = z; + else if (p->zpsm == 0x31) vram[zaddr] = (vram[zaddr] & 0xFF000000u) | (z & 0xFFFFFF); + else px_write(p->zpsm, p->zbp, p->fbw, (u32)x, (u32)y, z); + } + if (__builtin_expect(ptrace_on, 0) && x == ptrace_x && y == ptrace_y && p->fbp == ptrace_fbp && uya_frame_count() >= ptrace_from) + uya_log(" -> %08X (fb %d, z %d, a %d)", px_read(p->fpsm, p->fbp, p->fbw, (u32)x, (u32)y), write_fb, write_z, write_a); +} + +/* The same pipeline for the targets almost every pixel goes to: 32-bit colour + * (PSMCT32, no FBMSK) and 32/24-bit Z. The frame and Z addresses share the + * swizzle (Z's block is the colour block ^ 0x18), and the row's part of it is + * worked out once per row (rowaddr_t). Same results as pixel(). */ +typedef struct { u32 rowoff; const u8 *brow, *crow; } rowaddr_t; +static inline int fast_ok(const pcmd_t *p) { return p->fpsm == 0 && !p->date && (p->zpsm == 0x30 || p->zpsm == 0x31) && !p->fbmsk && !ptrace_on; } +AINL void rowaddr(const pcmd_t *p, int y, rowaddr_t *ra) +{ + ra->rowoff = ((u32)y >> 5) * p->fbw * 32; ra->brow = gs_block32[((u32)y >> 3) & 3]; ra->crow = gs_col32[y & 7]; +} +/* the conditions under which pixel() would use z (else it can be left at 0) */ +static inline int z_used(const pcmd_t *p) { return p->ztst != 1 || (p->zte && !p->zmsk) || ptrace_on || !fast_ok(p); } +AINL u32 z_conv(double zz) { return zz <= 0 ? 0 : (zz >= 4294967295.0 ? 0xFFFFFFFFu : (u32)zz); } +/* Specialised copies: a spec S (SP_ON and the flags) makes the pipeline's main + * switches constants, so each copy has only its own steps; S = 0 reads them + * all from the command. */ +#define SP_ON 1 +#define SP_TME 2 +#define SP_BIL 4 /* bilinear on level 0 only (else tsample decides) */ +#define SP_ABE 8 +#define SP_ZT 16 /* a depth test (ZTST 2 or 3) */ +#define SP_ZW 32 /* Z can be written (ZTE, no ZMSK) */ +#define SPF(S, bit, rt) (((S) & SP_ON) ? (((S) & (bit)) != 0) : (rt)) +static inline int spec_key(const pcmd_t *p) +{ + int S = SP_ON; + if (p->tme) { + int lin = (p->mmin == 1 || p->mmin == 4 || p->mmin == 5); + S |= SP_TME; + if ((p->mxl == 0 || p->mmin < 2) && lin == p->mmag && lin) S |= SP_BIL; + } + if (p->abe) S |= SP_ABE; + if (p->ztst != 1) S |= SP_ZT; + if (p->zte && !p->zmsk) S |= SP_ZW; + return S; +} +#define SPEC_LIST(M) SL_T(M, SP_ON) SL_T(M, SP_ON | SP_TME) SL_T(M, SP_ON | SP_TME | SP_BIL) +#define SL_T(M, b) SL_A(M, b) SL_A(M, b | SP_ABE) +#define SL_A(M, b) SL_Z(M, b) SL_Z(M, b | SP_ZT) +#define SL_Z(M, b) M(b) M(b | SP_ZW) +AINL void pixel_fast(u32 *__restrict vr, const pcmd_t *p, const rowaddr_t *ra, int x, u32 z, int r, int g, int b, int a, float u, float v, float q, int f, const int S) +{ + u32 off = ra->rowoff + ((u32)x >> 6) * 32, blk = ra->brow[((u32)x >> 3) & 7], col = ra->crow[x & 7]; + u32 zaddr = (((p->zbp + off + (blk ^ 0x18)) & 0x3FFF) << 6) | col; + if (SPF(S, SP_ZT, p->ztst != 1)) { + if (p->ztst == 0) return; + u32 zd = vr[zaddr], zs = z; + if (p->zpsm == 0x31) { zs &= 0xFFFFFF; zd &= 0xFFFFFF; } + if (p->ztst == 2 ? zs < zd : zs <= zd) return; + } + if (SPF(S, SP_TME, p->tme)) { + u32 t = (S & SP_BIL) ? tsample_lvl(p, 0, u, v, 1) : tsample(p, u, v, q); + int tr = t & 255, tg = (t >> 8) & 255, tb = (t >> 16) & 255, ta = t >> 24; + switch (p->tfx) { + case 0: r = (tr * r) >> 7; g = (tg * g) >> 7; b = (tb * b) >> 7; a = p->tcc ? (ta * a) >> 7 : a; break; + case 1: r = tr; g = tg; b = tb; a = p->tcc ? ta : a; break; + case 2: r = ((tr * r) >> 7) + a; g = ((tg * g) >> 7) + a; b = ((tb * b) >> 7) + a; a = p->tcc ? ta + a : a; break; + default: r = ((tr * r) >> 7) + a; g = ((tg * g) >> 7) + a; b = ((tb * b) >> 7) + a; a = p->tcc ? ta : a; break; + } + r = clamp255(r); g = clamp255(g); b = clamp255(b); a = clamp255(a); + } + if (p->fge) { + r = (r * f + p->fogr * (255 - f)) >> 8; g = (g * f + p->fogg * (255 - f)) >> 8; b = (b * f + p->fogb * (255 - f)) >> 8; + } + int write_fb = 1, write_z = SPF(S, SP_ZW, p->zte && !p->zmsk), write_a = 1; + if (p->ate) { + int pass; + switch (p->atst) { + case 0: pass = 0; break; + case 1: pass = 1; break; + case 2: pass = a < (int)p->aref; break; + case 3: pass = a <= (int)p->aref; break; + case 4: pass = a == (int)p->aref; break; + case 5: pass = a >= (int)p->aref; break; + case 6: pass = a > (int)p->aref; break; + default: pass = a != (int)p->aref; break; + } + if (!pass) { + switch (p->afail) { + case 0: return; + case 1: write_z = 0; break; + case 2: write_fb = 0; break; + case 3: write_z = 0; write_a = 0; break; + } + } + } + if (write_fb) { + u32 faddr = (((p->fbp + off + blk) & 0x3FFF) << 6) | col; + int da = 0; + if (SPF(S, SP_ABE, p->abe)) { + u32 old = vr[faddr]; + int cs[3] = {r, g, b}, cd[3] = {(int)(old & 255), (int)((old >> 8) & 255), (int)((old >> 16) & 255)}, o[3]; + da = (int)(old >> 24); + int cc = p->bc == 0 ? a : (p->bc == 1 ? da : (int)p->fix); + for (int k = 0; k < 3; k++) { + int A = p->ba == 0 ? cs[k] : (p->ba == 1 ? cd[k] : 0); + int B = p->bb == 0 ? cs[k] : (p->bb == 1 ? cd[k] : 0); + int D = p->bd == 0 ? cs[k] : (p->bd == 1 ? cd[k] : 0); + o[k] = (((A - B) * cc) >> 7) + D; + } + r = o[0]; g = o[1]; b = o[2]; + } else if (!write_a) da = (int)(vr[faddr] >> 24); + if (p->clampc) { r = clamp255(r); g = clamp255(g); b = clamp255(b); } + else { r &= 255; g &= 255; b &= 255; } + if (p->fba) a |= 0x80; + if (!write_a) a = da; + vr[faddr] = (u32)r | ((u32)g << 8) | ((u32)b << 16) | ((u32)(a & 255) << 24); + } + if (write_z) { + if (p->zpsm == 0x30) vr[zaddr] = z; + else vr[zaddr] = (vr[zaddr] & 0xFF000000u) | (z & 0xFFFFFF); + } +} + +/* a triangle's attributes as planes (value = dx * x + dy * y + c) */ +typedef struct { float rdx, rdy, rc, gdx, gdy, gc, bdx, bdy, bc, adx, ady, ac, fdx, fdy, fc, udx, udy, uc, vdx, vdy, vc, qdx, qdy, qc; double zA, zB, zC; int fr, fg, fb, fa; } tplanes_t; +/* a row of a triangle (x from xs to xe) and of a sprite, through pixel_fast; a + * row starts each attribute from its plane (only those the pixels use) */ +AINL void tri_row(const pcmd_t *p, int y, int xs, int xe, const tplanes_t *P, const int S) +{ + rowaddr_t ra; rowaddr(p, y, &ra); + u32 *__restrict vr = vram; /* GS memory is not the command's memory */ + const int need_z = (S & SP_ON) ? (S & (SP_ZT | SP_ZW)) != 0 : z_used(p), tme = SPF(S, SP_TME, p->tme), persp = tme && !p->fst, iip = p->iip; + const float fx = (float)xs, py = (float)y; + const float rdx = P->rdx, gdx = P->gdx, bdx = P->bdx, adx = P->adx, fdx = P->fdx, udx = P->udx, vdx = P->vdx, qdx = P->qdx; + const double zA = P->zA; + const int fr = P->fr, fg = P->fg, fb = P->fb, fa = P->fa; + float r = 0, g = 0, b = 0, a = 0, f = 0, u = 0, v = 0, q = 0; + double zz = 0; + if (iip) { r = rdx * fx + P->rdy * py + P->rc; g = gdx * fx + P->gdy * py + P->gc; b = bdx * fx + P->bdy * py + P->bc; a = adx * fx + P->ady * py + P->ac; } + if (p->fge) f = fdx * fx + P->fdy * py + P->fc; + if (tme) { u = udx * fx + P->udy * py + P->uc; v = vdx * fx + P->vdy * py + P->vc; q = qdx * fx + P->qdy * py + P->qc; } + if (need_z) zz = zA * xs + P->zB * y + P->zC; + for (int x = xs; x <= xe; x++) { + u32 z = need_z ? z_conv(zz) : 0; + float tu = u, tv = v; + if (persp && q != 0) { float iq = 1.0f / q; tu = u * iq; tv = v * iq; } + if (iip) pixel_fast(vr, p, &ra, x, z, (int)r, (int)g, (int)b, (int)a, tu, tv, q, (int)f, S); + else pixel_fast(vr, p, &ra, x, z, fr, fg, fb, fa, tu, tv, q, (int)f, S); + r += rdx; g += gdx; b += bdx; a += adx; f += fdx; u += udx; v += vdx; q += qdx; zz += zA; + } +} +AINL void sprite_row(const pcmd_t *p, int y, int ix0, int ix1, u32 z, int cr, int cg, int cb, int ca, int cf, float u0, float x0, float du, float vv, float q, const int S) +{ + rowaddr_t ra; rowaddr(p, y, &ra); + u32 *__restrict vr = vram; /* GS memory is not the command's memory */ + for (int x = ix0; x < ix1; x++) + pixel_fast(vr, p, &ra, x, z, cr, cg, cb, ca, u0 + (x - x0) * du, vv, q, cf, S); +} + +/* rows a worker draws */ +#define ROW_MINE(y) (nth == 1 || (((u32)(y) >> 2) % (u32)nth) == (u32)tid) + +static void vtx_uvq(const pcmd_t *p, const vtx_t *v, float *u, float *w, float *q) +{ + if (p->fst) { *u = v->u; *w = v->v; *q = 1.0f; } + else { *u = v->s * p->tw; *w = v->t * p->th; *q = v->q; } +} + +static void r_sprite(const pcmd_t *p, const vtx_t *V_, int tid, int nth) +{ + const vtx_t *a = &V_[0], *b = &V_[1]; + float x0 = a->x, x1 = b->x, y0 = a->y, y1 = b->y; + float u0, v0, q0, u1, v1, q1; + vtx_uvq(p, a, &u0, &v0, &q0); + vtx_uvq(p, b, &u1, &v1, &q1); + if (!p->fst) { u0 /= q1; v0 /= q1; u1 /= q1; v1 /= q1; } /* sprites use the last Q */ + float q = p->fst ? 1.0f : q1; + if (x0 > x1) { float t = x0; x0 = x1; x1 = t; t = u0; u0 = u1; u1 = t; } + if (y0 > y1) { float t = y0; y0 = y1; y1 = t; t = v0; v0 = v1; v1 = t; } + int ix0 = (int)ceilf(x0), ix1 = (int)ceilf(x1), iy0 = (int)ceilf(y0), iy1 = (int)ceilf(y1); + if (ix0 < p->sx0) ix0 = p->sx0; + if (iy0 < p->sy0) iy0 = p->sy0; + if (ix1 > p->sx1 + 1) ix1 = p->sx1 + 1; + if (iy1 > p->sy1 + 1) iy1 = p->sy1 + 1; + if (ix0 < 0) ix0 = 0; + if (iy0 < 0) iy0 = 0; + if (ix1 > 2048) ix1 = 2048; + if (iy1 > 2048) iy1 = 2048; + float du = x1 > x0 ? (u1 - u0) / (x1 - x0) : 0, dv = y1 > y0 ? (v1 - v0) / (y1 - y0) : 0; + if (fast_ok(p)) { + if (p->ztst == 0) return; + u32 z = b->z; int cr = b->r, cg = b->g, cb = b->b, ca = b->a, cf = b->f, S = spec_key(p); + for (int y = iy0; y < iy1; y++) { + if (!ROW_MINE(y)) continue; + float vv = v0 + (y - y0) * dv; + switch (S) { +#define SR(k) case k: sprite_row(p, y, ix0, ix1, z, cr, cg, cb, ca, cf, u0, x0, du, vv, q, k); break; + SPEC_LIST(SR) +#undef SR + default: sprite_row(p, y, ix0, ix1, z, cr, cg, cb, ca, cf, u0, x0, du, vv, q, 0); break; + } + } + return; + } + for (int y = iy0; y < iy1; y++) { + if (!ROW_MINE(y)) continue; + float vv = v0 + (y - y0) * dv; + for (int x = ix0; x < ix1; x++) + pixel(p, x, y, b->z, b->r, b->g, b->b, b->a, u0 + (x - x0) * du, vv, q, b->f); + } +} + +static void r_tri(const pcmd_t *p, const vtx_t *V_, int tid, int nth) +{ + const vtx_t *v0 = &V_[0], *v1 = &V_[1], *v2 = &V_[2]; + float area = (v1->x - v0->x) * (v2->y - v0->y) - (v2->x - v0->x) * (v1->y - v0->y); + if (fabsf(area) < 1e-6f) return; + float minx = fminf(v0->x, fminf(v1->x, v2->x)), maxx = fmaxf(v0->x, fmaxf(v1->x, v2->x)); + float miny = fminf(v0->y, fminf(v1->y, v2->y)), maxy = fmaxf(v0->y, fmaxf(v1->y, v2->y)); + int ix0 = (int)ceilf(minx), ix1 = (int)ceilf(maxx), iy0 = (int)ceilf(miny), iy1 = (int)ceilf(maxy); + if (ix0 < p->sx0) ix0 = p->sx0; + if (iy0 < p->sy0) iy0 = p->sy0; + if (ix1 > p->sx1 + 1) ix1 = p->sx1 + 1; + if (iy1 > p->sy1 + 1) iy1 = p->sy1 + 1; + if (ix0 < 0) ix0 = 0; + if (iy0 < 0) iy0 = 0; + if (ix1 > 2048) ix1 = 2048; + if (iy1 > 2048) iy1 = 2048; + if (ix0 >= ix1 || iy0 >= iy1) return; + /* barycentric weights as planes: w_k = A_k x + B_k y + C_k */ + float inv = 1.0f / area; + const vtx_t *V[3] = {v0, v1, v2}; + float A[3], B[3], C[3]; + for (int k = 0; k < 3; k++) { + const vtx_t *a = V[(k + 1) % 3], *b = V[(k + 2) % 3]; + /* w_k(px,py) = ((a.x - px)(b.y - py) - (b.x - px)(a.y - py)) / area */ + A[k] = (a->y - b->y) * inv; + B[k] = (b->x - a->x) * inv; + C[k] = (a->x * b->y - b->x * a->y) * inv; + } + /* attribute planes: value = dx * px + dy * py + c */ +#define PLANE(get, dx, dy, c0) do { float a0_ = (get(v0)), a1_ = (get(v1)), a2_ = (get(v2)); \ + dx = a0_ * A[0] + a1_ * A[1] + a2_ * A[2]; dy = a0_ * B[0] + a1_ * B[1] + a2_ * B[2]; c0 = a0_ * C[0] + a1_ * C[1] + a2_ * C[2]; } while (0) + float rdx = 0, rdy = 0, rc = 0, gdx = 0, gdy = 0, gc = 0, bdx = 0, bdy = 0, bc_ = 0, adx = 0, ady = 0, ac = 0, fdx, fdy, fc; + if (p->iip) { +#define GR(v) (float)(v)->r +#define GG(v) (float)(v)->g +#define GB(v) (float)(v)->b +#define GA(v) (float)(v)->a + PLANE(GR, rdx, rdy, rc); PLANE(GG, gdx, gdy, gc); PLANE(GB, bdx, bdy, bc_); PLANE(GA, adx, ady, ac); + } +#define GF(v) (float)(v)->f + PLANE(GF, fdx, fdy, fc); + /* z in double: 32-bit depths need more than a float's precision */ + double zA = 0, zB = 0, zC = 0; + if (!fast_ok(p) || z_used(p)) { + double d[3] = {v0->z, v1->z, v2->z}; + double Ad[3], Bd[3], Cd[3], invd = 1.0 / ((double)(v1->x - v0->x) * (v2->y - v0->y) - (double)(v2->x - v0->x) * (v1->y - v0->y)); + for (int k = 0; k < 3; k++) { + const vtx_t *a = V[(k + 1) % 3], *b = V[(k + 2) % 3]; + Ad[k] = ((double)a->y - b->y) * invd; Bd[k] = ((double)b->x - a->x) * invd; Cd[k] = ((double)a->x * b->y - (double)b->x * a->y) * invd; + } + for (int k = 0; k < 3; k++) { zA += d[k] * Ad[k]; zB += d[k] * Bd[k]; zC += d[k] * Cd[k]; } + } + float udx = 0, udy = 0, uc = 0, vdx = 0, vdy = 0, vc = 0, qdx = 0, qdy = 0, qc = 1; + if (p->tme) { + float uu[3], vv[3], qq[3]; + for (int k = 0; k < 3; k++) vtx_uvq(p, V[k], &uu[k], &vv[k], &qq[k]); + float a0_, a1_, a2_; + a0_ = uu[0]; a1_ = uu[1]; a2_ = uu[2]; + udx = a0_ * A[0] + a1_ * A[1] + a2_ * A[2]; udy = a0_ * B[0] + a1_ * B[1] + a2_ * B[2]; uc = a0_ * C[0] + a1_ * C[1] + a2_ * C[2]; + a0_ = vv[0]; a1_ = vv[1]; a2_ = vv[2]; + vdx = a0_ * A[0] + a1_ * A[1] + a2_ * A[2]; vdy = a0_ * B[0] + a1_ * B[1] + a2_ * B[2]; vc = a0_ * C[0] + a1_ * C[1] + a2_ * C[2]; + if (!p->fst) { + a0_ = qq[0]; a1_ = qq[1]; a2_ = qq[2]; + qdx = a0_ * A[0] + a1_ * A[1] + a2_ * A[2]; qdy = a0_ * B[0] + a1_ * B[1] + a2_ * B[2]; qc = a0_ * C[0] + a1_ * C[1] + a2_ * C[2]; + } + } + /* Pixel centres on an edge belong to one triangle only (the GS's top-left + * rule): to the triangle that edge is a left edge of (w grows with x), or a + * top edge (horizontal, w grows with y). Drawing them twice put a darker or + * brighter seam on every shared edge of blended geometry (the diagonal of + * a quad). eps[k] is the weight a pixel must reach for edge k. */ + float eps[3]; + for (int k = 0; k < 3; k++) eps[k] = (A[k] > 0 || (A[k] == 0 && B[k] > 0)) ? -1e-5f : 1e-5f; + const int fast = fast_ok(p), S = fast ? spec_key(p) : 0; + if (fast && p->ztst == 0) return; /* nothing passes */ + tplanes_t P = { rdx, rdy, rc, gdx, gdy, gc, bdx, bdy, bc_, adx, ady, ac, fdx, fdy, fc, udx, udy, uc, vdx, vdy, vc, qdx, qdy, qc, zA, zB, zC, + V_[2].r, V_[2].g, V_[2].b, V_[2].a }; + float iA[3]; + for (int k = 0; k < 3; k++) iA[k] = A[k] != 0 ? 1.0f / A[k] : 0; + for (int y = iy0; y < iy1; y++) { + if (!ROW_MINE(y)) continue; + float py = (float)y; + /* the row's span: every w_k >= eps[k] */ + float lo = (float)ix0, hi = (float)(ix1 - 1); + int empty = 0; + for (int k = 0; k < 3; k++) { + float rest = B[k] * py + C[k]; + if (A[k] > 0) { float bnd = (eps[k] - rest) * iA[k]; if (bnd > lo) lo = bnd; } + else if (A[k] < 0) { float bnd = (eps[k] - rest) * iA[k]; if (bnd < hi) hi = bnd; } + else if (rest < eps[k]) { empty = 1; break; } + } + if (empty) continue; + int xs = (int)ceilf(lo), xe = (int)floorf(hi); + if (xs < ix0) xs = ix0; + if (xe > ix1 - 1) xe = ix1 - 1; + /* the bounds are exact to rounding: check the end pixels with the weights */ + while (xs <= xe) { + float px = (float)xs; + if (A[0] * px + B[0] * py + C[0] >= eps[0] && A[1] * px + B[1] * py + C[1] >= eps[1] && A[2] * px + B[2] * py + C[2] >= eps[2]) break; + xs++; + } + while (xe >= xs) { + float px = (float)xe; + if (A[0] * px + B[0] * py + C[0] >= eps[0] && A[1] * px + B[1] * py + C[1] >= eps[1] && A[2] * px + B[2] * py + C[2] >= eps[2]) break; + xe--; + } + if (xs > xe) continue; + if (fast) { + switch (S) { +#define TR(k) case k: tri_row(p, y, xs, xe, &P, k); break; + SPEC_LIST(TR) +#undef TR + default: tri_row(p, y, xs, xe, &P, 0); break; + } + continue; + } + float fx = (float)xs; + float r = rdx * fx + rdy * py + rc, g = gdx * fx + gdy * py + gc, b = bdx * fx + bdy * py + bc_, a = adx * fx + ady * py + ac; + float f = fdx * fx + fdy * py + fc; + float u = udx * fx + udy * py + uc, v = vdx * fx + vdy * py + vc, q = qdx * fx + qdy * py + qc; + double zz = zA * xs + zB * y + zC; + int fr = V_[2].r, fg = V_[2].g, fb = V_[2].b, fa = V_[2].a; + for (int x = xs; x <= xe; x++) { + u32 z = zz <= 0 ? 0 : (zz >= 4294967295.0 ? 0xFFFFFFFFu : (u32)zz); + float tu = u, tv = v; + if (p->tme && !p->fst && q != 0) { float iq = 1.0f / q; tu = u * iq; tv = v * iq; } + if (p->iip) pixel(p, x, y, z, (int)r, (int)g, (int)b, (int)a, tu, tv, q, (int)f); + else pixel(p, x, y, z, fr, fg, fb, fa, tu, tv, q, (int)f); + r += rdx; g += gdx; b += bdx; a += adx; f += fdx; u += udx; v += vdx; q += qdx; zz += zA; + } + } +#undef PLANE +} + +static void r_line(const pcmd_t *p, const vtx_t *V_, int tid, int nth) +{ + const vtx_t *a = &V_[0], *b = &V_[1]; + float dx = b->x - a->x, dy = b->y - a->y; + int n = (int)ceilf(fmaxf(fabsf(dx), fabsf(dy))); + if (n <= 0) n = 1; + float ua, va, qa, ub, vb, qb; + vtx_uvq(p, a, &ua, &va, &qa); + vtx_uvq(p, b, &ub, &vb, &qb); + if (!p->fst) { ua /= qa; va /= qa; ub /= qb; vb /= qb; } + float q = p->fst ? 1.0f : qb; + for (int i = 0; i < n; i++) { + float t = (float)i / n; + int x = (int)floorf(a->x + dx * t + 0.5f), y = (int)floorf(a->y + dy * t + 0.5f); + if (x < p->sx0 || x > p->sx1 || y < p->sy0 || y > p->sy1 || x < 0 || y < 0 || x > 2047 || y > 2047 || !ROW_MINE(y)) continue; + pixel(p, x, y, b->z, (int)(a->r + (b->r - a->r) * t), (int)(a->g + (b->g - a->g) * t), (int)(a->b + (b->b - a->b) * t), + (int)(a->a + (b->a - a->a) * t), ua + (ub - ua) * t, va + (vb - va) * t, q, b->f); + } +} + +static void r_point(const pcmd_t *p, const vtx_t *V_, int tid, int nth) +{ + const vtx_t *v = &V_[0]; + int x = (int)floorf(v->x), y = (int)floorf(v->y); + if (x < p->sx0 || x > p->sx1 || y < p->sy0 || y > p->sy1 || x < 0 || y < 0 || x > 2047 || y > 2047 || !ROW_MINE(y)) return; + float u, w, q; + vtx_uvq(p, v, &u, &w, &q); + if (!p->fst && q != 0) { u /= q; w /= q; } + pixel(p, x, y, v->z, v->r, v->g, v->b, v->a, u, w, q, v->f); +} + +static void run_cmd(const pcmd_t *p, const pprim_t *pr, int tid, int nth) +{ + ptrace_prim = pr; + if (nth > 1 && !(dbg_drain_all & 16)) { /* skip a primitive none of whose rows (4-row bands, dealt round-robin) are this thread's */ + const vtx_t *v = pr->v; + int nv = pr->type == 3 ? 3 : (pr->type == 0 ? 1 : 2); + float y0 = v[0].y, y1 = v[0].y; + for (int k = 1; k < nv; k++) { if (v[k].y < y0) y0 = v[k].y; if (v[k].y > y1) y1 = v[k].y; } + int a = (int)floorf(y0) - 1, b = (int)ceilf(y1) + 1; + if (a < p->sy0) a = p->sy0; + if (b > p->sy1) b = p->sy1; + if (a < 0) a = 0; + if (b > 2047) b = 2047; + if (a > b) return; + int b0 = a >> 2, b1 = b >> 2; + if (b1 - b0 + 1 < nth) { + int mine = 0; + for (int k = b0; k <= b1; k++) if (k % nth == tid) { mine = 1; break; } + if (!mine) return; + } + } + switch (pr->type) { + case 0: r_point(p, pr->v, tid, nth); break; + case 1: r_line(p, pr->v, tid, nth); break; + case 3: r_tri(p, pr->v, tid, nth); break; + case 6: r_sprite(p, pr->v, tid, nth); break; + } +} + +/* ---- worker threads ---- */ +#define QN 8192 +#define SN (QN * 2) +static pprim_t *cq; /* ring of primitives */ +static pcmd_t *sring; /* ring of states the primitives use (a new one only when the state changes) */ +static u32 s_head; +/* the counters on cache lines of their own: the producer writes q_head and + * each worker its q_tail all the time, and sharing a line makes every write + * take the line from the other cores */ +typedef struct { volatile u32 v; char pad_[60]; } __attribute__((aligned(64))) line_u32; +static line_u32 q_head_; /* written by the producer */ +static line_u32 q_tail_[16]; /* each worker's progress */ +#define q_head (q_head_.v) +static int nworkers; +static volatile int gs_quit; +static int dbg_race(u32 d0, u32 d1, const char *what, u32 bp) +{ + if (!nworkers || !sring || !cq) return 0; + u32 lo = q_head; for (int i = 0; i < nworkers; i++) { u32 t = q_tail_[i].v; if (q_head - t > q_head - lo) lo = t; } + for (u32 q = lo; q != q_head; q++) { + const pcmd_t *S = &sring[cq[q % QN].st % SN]; + u32 f0 = S->fbp / 32, f1 = (S->fbp + area_blocks(S->fpsm, S->fbw, S->sy1 + 1) - 1) / 32; + if (f0 <= d1 && d0 <= f1) { uya_log("RACE: %s at %04X (pages %u-%u) while prim %u draws FBP %04X (pages %u-%u), %u queued", what, bp, d0, d1, q, S->fbp, f0, f1, q_head - lo); gs_drain(); return 1; } + } + return 0; +} + +#ifdef _WIN32 +#include +static void cpu_relax(int n) { if (n < 64) __asm__ volatile("pause"); else Sleep(0); } +#else +#include +#include +#include +static void cpu_relax(int n) { if (n < 64) __asm__ volatile("pause"); else sched_yield(); } +#include +#include +#endif + +/* parking an idle thread (so idle drawing threads leave the cores to the + * game and the GS thread) and waking it */ +typedef struct __attribute__((aligned(64))) { + volatile int sleeping; +#ifdef _WIN32 + HANDLE ev; +#else + sem_t sem; +#endif +} park_t; +static void park_init(park_t *k) +{ + k->sleeping = 0; +#ifdef _WIN32 + k->ev = CreateEventA(0, FALSE, FALSE, 0); +#else + sem_init(&k->sem, 0, 0); +#endif +} +static void park_wait(park_t *k) /* returns on a wake-up or after 2 ms */ +{ +#ifdef _WIN32 + WaitForSingleObject(k->ev, 2); +#else + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 2000000; if (ts.tv_nsec >= 1000000000) { ts.tv_nsec -= 1000000000; ts.tv_sec++; } + while (sem_timedwait(&k->sem, &ts) == -1 && errno == EINTR) ; +#endif +} +static void park_wake(park_t *k) +{ + if (!__atomic_load_n(&k->sleeping, __ATOMIC_ACQUIRE)) return; +#ifdef _WIN32 + SetEvent(k->ev); +#else + sem_post(&k->sem); +#endif +} +static park_t wpark[16]; + +static void worker_loop(int tid) +{ + u32 t = 0; + int spin = 0; + for (;;) { + u32 h = __atomic_load_n(&q_head, __ATOMIC_ACQUIRE); + if (t == h) { + if (spin++ < 2000) { __asm__ volatile("pause"); continue; } + __atomic_store_n(&wpark[tid].sleeping, 1, __ATOMIC_SEQ_CST); + if (__atomic_load_n(&q_head, __ATOMIC_SEQ_CST) == t) park_wait(&wpark[tid]); + __atomic_store_n(&wpark[tid].sleeping, 0, __ATOMIC_RELEASE); + continue; + } + spin = 0; + while (t != h) { const pprim_t *pr = &cq[t % QN]; run_cmd(&sring[pr->st % SN], pr, tid, nworkers); t++; if ((t & 255) == 0) __atomic_store_n(&q_tail_[tid].v, t, __ATOMIC_RELEASE); } + __atomic_store_n(&q_tail_[tid].v, t, __ATOMIC_RELEASE); + } +} +#ifdef _WIN32 +static DWORD WINAPI worker_main(LPVOID a) { worker_loop((int)(intptr_t)a); return 0; } +#else +static void *worker_main(void *a) { worker_loop((int)(intptr_t)a); return 0; } +#endif + +static void mtgs_init(int cores); +static void gs_threads_init(void) +{ + { const char *d = getenv("UYA_DRAINALL"); dbg_drain_all = d ? atoi(d) : 0; } + { const char *t = getenv("UYA_PIXTRACE"); if (t && sscanf(t, "%d,%d,%x,%u", &ptrace_x, &ptrace_y, &ptrace_fbp, &ptrace_from) >= 3) ptrace_on = 1; } + const char *e = getenv("UYA_GS_THREADS"); + int n; + if (e) n = atoi(e); + else { +#ifdef _WIN32 + SYSTEM_INFO si; GetSystemInfo(&si); n = (int)si.dwNumberOfProcessors - 1; +#else + n = (int)sysconf(_SC_NPROCESSORS_ONLN) - 1; +#endif + if (n > 8) n = 8; + } + { + int cores; +#ifdef _WIN32 + SYSTEM_INFO si2; GetSystemInfo(&si2); cores = (int)si2.dwNumberOfProcessors; +#else + cores = (int)sysconf(_SC_NPROCESSORS_ONLN); +#endif + mtgs_init(cores); + if (mtgs && !e && n > 1) n--; /* the GS thread takes a core */ + } + if (n < 1) n = 0; + if (n > 16) n = 16; + if (n == 1) n = 0; /* one worker gains nothing over drawing here */ + if (gsgl_on) n = 0; /* the GPU draws */ + nworkers = n; + if (!n) return; + cq = malloc(sizeof(pprim_t) * QN); + for (int i = 0; i < n; i++) park_init(&wpark[i]); + for (int i = 0; i < n; i++) { +#ifdef _WIN32 + CreateThread(0, 0, worker_main, (LPVOID)(intptr_t)i, 0, 0); +#else + pthread_t th; pthread_create(&th, 0, worker_main, (void *)(intptr_t)i); pthread_detach(th); +#endif + } + uya_log("GS: drawing on %d threads", n); +} + +static inline int gs_busy(void) +{ + for (int i = 0; i < nworkers; i++) if (__atomic_load_n(&q_tail_[i].v, __ATOMIC_ACQUIRE) != q_head) return 1; + return 0; +} +/* wait until the workers have drawn everything queued */ +static void retire_free(int all); +static void gs_drain(void) +{ + if (!nworkers) return; + if (gs_busy()) n_drain_wait++; + int spin = 0; + while (gs_busy()) cpu_relax(spin++); + retire_free(1); + memset(page_drawn, 0, sizeof page_drawn); + set_drawn_target(); +} +static struct { void *px; u32 head; size_t n; } retired[4096]; +static u32 n_retired; +static void retire_free(int all) +{ + u32 lo = q_head; + if (!all) for (int i = 0; i < nworkers; i++) { u32 t = __atomic_load_n(&q_tail_[i].v, __ATOMIC_ACQUIRE); if (q_head - t > q_head - lo) lo = t; } + u32 k = 0; + for (u32 i = 0; i < n_retired; i++) { + if ((s32)(lo - retired[i].head) >= 0) { if (dbg_drain_all & 4) { u32 *q = retired[i].px; for (size_t j = 0; j < retired[i].n / 4; j++) q[j] = 0xFFFF00FFu; static void *pz[64]; static u32 npz; if (pz[npz & 63]) free(pz[npz & 63]); pz[npz++ & 63] = q; } else tex_free(retired[i].px, retired[i].n); } + else retired[k++] = retired[i]; + } + n_retired = k; +} +static void retire(void *px) +{ + st_dirty_force(); + if (!nworkers) { tex_free(px, retire_n); return; } + if (n_retired == 4096) retire_free(0); + if (n_retired == 4096) { gs_drain(); } + retired[n_retired].px = px; retired[n_retired].head = q_head; retired[n_retired].n = retire_n; n_retired++; +} + +static void submit(const pprim_t *p) +{ + if (!nworkers) { + if (!uya_timing) { run_cmd(&sring[p->st % SN], p, 0, 1); return; } + unsigned long long t_ = rdtsc_(); run_cmd(&sring[p->st % SN], p, 0, 1); t_draw += rdtsc_() - t_; return; + } + u32 h = q_head; + int spin = 0; + for (;;) { /* wait for room: no worker may be QN behind */ + int full = 0; + for (int i = 0; i < nworkers; i++) if (h - __atomic_load_n(&q_tail_[i].v, __ATOMIC_ACQUIRE) >= QN - 1) full = 1; + if (!full) break; + cpu_relax(spin++); + } + cq[h % QN] = *p; + __atomic_store_n(&q_head, h + 1, __ATOMIC_SEQ_CST); + for (int i = 0; i < nworkers; i++) park_wake(&wpark[i]); +} + +/* the command for the current primitive: pipeline state and texture levels */ +static int cmd_setup(pcmd_t *p) +{ + u64 pr = prmodecont ? prim : ((prim & 7) | (prmode & ~7ull)); + const gs_ctx_t *c = &ctx[BITS(pr, 9, 1)]; + memset(p, 0, sizeof *p); + p->tme = BITS(pr, 4, 1); p->fge = BITS(pr, 5, 1); p->abe = BITS(pr, 6, 1); p->fst = BITS(pr, 8, 1); p->iip = BITS(pr, 3, 1); + p->fbp = BITS(c->frame, 0, 9) * 32; p->fbw = BITS(c->frame, 16, 6); p->fpsm = BITS(c->frame, 24, 6); p->fbmsk = (u32)(c->frame >> 32); + p->zbp = BITS(c->zbuf, 0, 9) * 32; p->zpsm = BITS(c->zbuf, 24, 4) | 0x30; p->zmsk = BITS(c->zbuf, 32, 1); + p->ate = BITS(c->test, 0, 1); p->atst = BITS(c->test, 1, 3); p->aref = BITS(c->test, 4, 8); p->afail = BITS(c->test, 12, 2); + p->zte = BITS(c->test, 16, 1); p->ztst = BITS(c->test, 17, 2); + p->date = BITS(c->test, 14, 1); p->datm = BITS(c->test, 15, 1); + p->ba = BITS(c->alpha, 0, 2); p->bb = BITS(c->alpha, 2, 2); p->bc = BITS(c->alpha, 4, 2); p->bd = BITS(c->alpha, 6, 2); p->fix = BITS(c->alpha, 32, 8); + p->tfx = BITS(c->tex0, 35, 2); p->tcc = BITS(c->tex0, 34, 1); p->fba = BITS(c->fba, 0, 1); p->clampc = BITS(colclamp, 0, 1); + p->sx0 = (int)BITS(c->scissor, 0, 11); p->sx1 = (int)BITS(c->scissor, 16, 11); + p->sy0 = (int)BITS(c->scissor, 32, 11); p->sy1 = (int)BITS(c->scissor, 48, 11); + p->tw = (float)(1u << BITS(c->tex0, 26, 4)); p->th = (float)(1u << BITS(c->tex0, 30, 4)); + p->fogr = (u8)BITS(fogcol, 0, 8); p->fogg = (u8)BITS(fogcol, 8, 8); p->fogb = (u8)BITS(fogcol, 16, 8); + if (!p->zte) { p->ztst = 1; } /* depth test off: always pass */ + prim_serial++; + { /* the pages this target writes count as changed (texture cache) */ + static u64 lf, lz, ls; static int lzte; + if (c->frame != lf || c->zbuf != lz || c->scissor != ls || (int)p->zte != lzte) { + u32 h = (u32)p->sy1 + 1; + if (lf) mark_blocks((u32)BITS(lf, 0, 9) * 32, area_blocks(BITS(lf, 24, 6), BITS(lf, 16, 6), (u32)BITS(ls, 48, 11) + 1)); + mark_blocks(p->fbp, area_blocks(p->fpsm, p->fbw, h)); n_mark_draw++; + if (p->zte) mark_blocks(p->zbp, area_blocks(p->zpsm, p->fbw, h)); + cur_target[0] = p->fbp; cur_target[1] = area_blocks(p->fpsm, p->fbw, h); + cur_target[2] = p->zbp; cur_target[3] = (p->zte || !p->zmsk) ? area_blocks(p->zpsm, p->fbw, h) : 0; + cur_target[4] = p->fbw | (p->fpsm << 8); cur_target[5] = p->fbw | (p->zpsm << 8); + if (layout_conflict(cur_target[0], cur_target[1], (u16)cur_target[4]) || + (cur_target[3] && layout_conflict(cur_target[2], cur_target[3], (u16)cur_target[5]))) { n_layout_drain++; gs_drain(); } + set_drawn_target(); + lf = c->frame; lz = c->zbuf; ls = c->scissor; lzte = (int)p->zte; + } + } + if (p->tme) { + u64 t1 = c->tex1; + p->lcm = BITS(t1, 0, 1); p->mxl = BITS(t1, 2, 3); p->mmag = BITS(t1, 5, 1); p->mmin = BITS(t1, 6, 3); p->l = BITS(t1, 19, 2); + if (p->mxl > 6) p->mxl = 6; + p->k = (float)((s32)(BITS(t1, 32, 12) << 20) >> 20) / 16.0f; + p->wms = BITS(c->clamp, 0, 2); p->wmt = BITS(c->clamp, 2, 2); + p->minu = (int)BITS(c->clamp, 4, 10); p->maxu = (int)BITS(c->clamp, 14, 10); + p->minv = (int)BITS(c->clamp, 24, 10); p->maxv = (int)BITS(c->clamp, 34, 10); + p->tex0 = c->tex0; p->tex1 = c->tex1; p->miptbp1 = c->miptbp1; p->miptbp2 = c->miptbp2; + int nl = (p->mmin >= 2) ? p->mxl : 0; + if (gsgl_on) { + u32 tpsm_ = BITS(c->tex0, 20, 6); + if (tpsm_ == 0x13 || tpsm_ == 0x1B || tpsm_ == 0x14 || tpsm_ == 0x24 || tpsm_ == 0x2C) clut_flush(); /* (only a paletted texture uses it) */ + p->clamp = c->clamp; p->texa = texa; + if (gsgl_tex_setup(p)) { nl = -1; p->mxl = 0; } /* read from a GPU target */ + } + for (int lv = 0; lv <= nl; lv++) { + u32 b_, bw_, lw_, lh_; tex_level(c, lv, &b_, &bw_, &lw_, &lh_); + p->lv[lv].lw = (int)lw_; p->lv[lv].lh = (int)lh_; + /* GL: a level in memory a GPU target owns is decoded only if a batch needs it */ + if (gsgl_on && gsgl_tex_gpu(BITS(c->tex0, 20, 6), b_, bw_, lw_, lh_)) { p->defer |= (u8)(1 << lv); continue; } + const tcache_t *e = tc_get(c, lv); + p->lv[lv].px = e->px; p->lv[lv].w = (int)e->w; p->lv[lv].h = (int)e->h; + p->lv[lv].tci = (int)(e - tc); p->lv[lv].ser = e->ser; + if (gsgl_on) gsgl_tex_touch(&p->lv[lv]); + } + if (nl >= 0 && nl < (int)p->mxl) p->mxl = (u8)nl; + } + return 1; +} + +/* memcmp of two commands (cmd_setup zeroes the padding), a word at a time */ +static inline int cmd_same(const pcmd_t *a, const pcmd_t *b) +{ + const u32 *x = (const u32 *)a, *y = (const u32 *)b; + u32 d = 0; + for (u32 i = 0; i < sizeof *a / 4; i++) d |= x[i] ^ y[i]; + return d == 0; +} +static int st_dirty = 1, st_tme; +static u32 st_epoch; +static void st_dirty_force(void) { st_dirty = 1; } +static void vertex_kick(u32 X, u32 Y, u32 Z, u8 F, int draw) +{ + u64 pr = prmodecont ? prim : ((prim & 7) | (prmode & ~7ull)); + const gs_ctx_t *c = &ctx[BITS(pr, 9, 1)]; + vtx_t v; + v.x = ((float)X - (float)BITS(c->xyoffset, 0, 16)) / 16.0f; + v.y = ((float)Y - (float)BITS(c->xyoffset, 32, 16)) / 16.0f; + v.z = Z; + v.r = (u8)BITS(rgbaq, 0, 8); v.g = (u8)BITS(rgbaq, 8, 8); v.b = (u8)BITS(rgbaq, 16, 8); v.a = (u8)BITS(rgbaq, 24, 8); + { u32 s = (u32)st, t = (u32)(st >> 32); memcpy(&v.s, &s, 4); memcpy(&v.t, &t, 4); } + v.q = qreg; + v.u = (float)BITS(uv, 0, 14) / 16.0f; + v.v = (float)BITS(uv, 16, 14) / 16.0f; + v.f = F; + u32 type = (u32)(prim & 7); + static const int need[8] = {1, 2, 2, 3, 3, 3, 2, 1}; + vq[vcount++] = v; + if (vcount < need[type]) return; + if (draw && type != 7) { + pprim_t pc; + static const u8 kind[8] = {0, 1, 1, 3, 3, 3, 6, 0}; + stat_prims++; + if (!sring) sring = calloc(SN, sizeof(pcmd_t)); + /* a new state only when a register changed, or GS memory under the texture */ + if (st_dirty || (st_tme && st_epoch != dirty_epoch)) { + pcmd_t ns; + cmd_setup(&ns); + if (!s_head || !cmd_same(&ns, &sring[(s_head - 1) % SN])) { sring[s_head % SN] = ns; s_head++; } + st_dirty = 0; st_tme = ns.tme; st_epoch = dirty_epoch; + } + pc.st = s_head - 1; + { + static int left = -1; + if (left < 0) { const char *e = getenv("UYA_SPRLOG"); left = e ? atoi(e) : 0; } + if (left > 0 && kind[type] == 6) { + const pcmd_t *S = &sring[pc.st % SN]; + left--; + uya_log("sprite (%s) (%.1f,%.1f)-(%.1f,%.1f) z %X rgba %u,%u,%u,%u uv %.1f,%.1f-%.1f,%.1f st %g,%g q %g TME %u FST %u TEX0 %016llX ABE %u ATE %u/%u ZTE %u/%u FRAME %llX", gs_src_int, + vq[0].x, vq[0].y, vq[1].x, vq[1].y, vq[1].z, vq[1].r, vq[1].g, vq[1].b, vq[1].a, vq[0].u, vq[0].v, vq[1].u, vq[1].v, vq[0].s, vq[0].t, vq[1].q, + S->tme, S->fst, (unsigned long long)ctx[BITS(pr, 9, 1)].tex0, S->abe, S->ate, S->atst, S->zte, S->ztst, (unsigned long long)ctx[BITS(pr, 9, 1)].frame); + } + } + { /* testing: UYA_PRIMLOG=v,n[,1] logs n primitives from vsync v (,1: only FST ones, the 2D overlays) */ + static int left = -1, fst_only; static unsigned from; + if (left < 0) { const char *e = getenv("UYA_PRIMLOG"); left = 0; if (e) sscanf(e, "%u,%d,%d", &from, &left, &fst_only); } + if (left > 0 && uya_frame_count() >= from && (!fst_only || BITS(pr, 8, 1))) { + const pcmd_t *S = &sring[pc.st % SN]; + left--; + uya_log("prim %u (%.1f,%.1f)(%.1f,%.1f)(%.1f,%.1f) rgba %u,%u,%u,%u uv %.1f,%.1f TME %u FST %u TEX0 %016llX ABE %u ATE %u/%u/%u ZTE %u/%u FRAME %llX ALPHA %llX TEXA %llX", kind[type], + vq[0].x, vq[0].y, vq[1].x, vq[1].y, vq[2].x, vq[2].y, vq[0].r, vq[0].g, vq[0].b, vq[0].a, vq[0].u, vq[0].v, + S->tme, S->fst, (unsigned long long)ctx[BITS(pr, 9, 1)].tex0, S->abe, S->ate, S->atst, S->aref, S->zte, S->ztst, (unsigned long long)ctx[BITS(pr, 9, 1)].frame, + (unsigned long long)ctx[BITS(pr, 9, 1)].alpha, (unsigned long long)texa); + } + } + pc.type = kind[type]; + pc.v[0] = vq[0]; pc.v[1] = vq[1]; pc.v[2] = vq[2]; + if (kind[type] == 3) { + const vtx_t *v0 = &vq[0], *v1 = &vq[1], *v2 = &vq[2]; + float area = (v1->x - v0->x) * (v2->y - v0->y) - (v2->x - v0->x) * (v1->y - v0->y); + static int left = -1; + if (left < 0) { const char *e = getenv("UYA_BIGTRI"); left = e ? atoi(e) : 0; } + if (left > 0 && fabsf(area) > 60000.0f) { + left--; + uya_log("big tri (%s): (%.1f,%.1f,%08X q %g) (%.1f,%.1f,%08X q %g) (%.1f,%.1f,%08X q %g) prim %llX", gs_src_int, + v0->x, v0->y, v0->z, v0->q, v1->x, v1->y, v1->z, v1->q, v2->x, v2->y, v2->z, v2->q, (unsigned long long)pr); + } + } +#ifdef GS_PRIM_HOOK + GS_PRIM_HOOK(&sring[pc.st % SN], &pc, &ctx[BITS(pr, 9, 1)]); +#endif + if (gsgl_on) gsgl_prim(&sring[pc.st % SN], pc.st, &pc); + else submit(&pc); + } + switch (type) { + case 2: vq[0] = vq[1]; vcount = 1; break; /* line strip */ + case 4: vq[0] = vq[1]; vq[1] = vq[2]; vcount = 2; break; /* triangle strip */ + case 5: vq[1] = vq[2]; vcount = 2; break; /* fan keeps vertex 0 */ + default: vcount = 0; break; + } +} + +/* ---- transfers ---- */ +static u32 trx_x, trx_y, trx_active; +static u8 trx_acc[8]; +static u32 trx_nacc; + +/* Uploads: games re-send the same texture to the same place every frame. An + * upload whose data (hash) and place match the last one there, with nothing + * else written to those pages since, leaves GS memory as it was, so it does + * not count as a change for the texture cache. */ +static u32 trx_pend, trx_rbp, trx_rn, trx_hash; +static struct { u64 key; u32 hash, sum, used; } up_rec[1024]; +static u32 pages_sum(u32 bp, u32 n) +{ + u32 p0 = bp / 32, p1 = (bp + (n ? n : 1) - 1) / 32, sum = 0; + if (p1 > 511) p1 = 511; + for (u32 p = p0; p <= p1; p++) sum = sum * 31 + page_gen[p]; + return sum; +} +static u32 si_hash[0x4000]; /* testing: the VRAM StoreImage saved, by block (UYA_SICHECK) */ +static u32 vhash(u32 bp, u32 nb) { u32 h = 2166136261u; for (u32 i = bp * 64; i < (bp + nb) * 64 && i < (1u << 20); i++) h = (h ^ vram[i]) * 16777619u; return h; } +static void trx_finish(void) +{ + { static int on = -1; if (on < 0) on = getenv("UYA_SICHECK") != 0; + u32 dbp = BITS(bitbltbuf, 32, 14); + if (on && si_hash[dbp] && BITS(bitbltbuf, 48, 6) == 1) { uya_log("restore %04X: VRAM %s the saved data", dbp, (vhash(dbp, 0x40) | 1) == si_hash[dbp] ? "matches" : "DIFFERS from"); si_hash[dbp] = 0; } + } + trx_pend = 0; + u64 key = (bitbltbuf >> 32) ^ ((u64)(trxpos >> 32) << 20) ^ (trxreg * 0x9E3779B97F4A7C15ull); + u32 i = (u32)(key ^ (key >> 29)) & 1023, k; + for (k = 0; k < 8; k++) { u32 j = (i + k) & 1023; if (up_rec[j].used && up_rec[j].key == key) { i = j; break; } } + if (k < 8 && !(dbg_drain_all & 32) && up_rec[i].hash == trx_hash && up_rec[i].sum == pages_sum(trx_rbp, trx_rn)) { if (trxlog_on()) uya_log(" upload done (same data, %08X)", trx_hash); return; } /* same data again */ + if (trxlog_on()) uya_log(" upload done (%08X), marks blocks %04X+%X", trx_hash, trx_rbp, trx_rn); + if (k == 8) { /* a free slot near i, else i */ + for (k = 0; k < 8; k++) if (!up_rec[(i + k) & 1023].used) break; + i = (i + (k < 8 ? k : 0)) & 1023; + } + mark_blocks(trx_rbp, trx_rn); n_mark_trx++; + up_rec[i].key = key; up_rec[i].hash = trx_hash; up_rec[i].sum = pages_sum(trx_rbp, trx_rn); up_rec[i].used = 1; +} + +static void trx_start(u32 dir) +{ + { /* wait for queued draws only if they write what this reads or writes */ + u32 dbp = BITS(bitbltbuf, 32, 14), dbw = BITS(bitbltbuf, 48, 6), dpsm = BITS(bitbltbuf, 56, 6); + u32 sbp = BITS(bitbltbuf, 0, 14), sbw = BITS(bitbltbuf, 16, 6), spsm = BITS(bitbltbuf, 24, 6); + u32 h = BITS(trxreg, 32, 12); + if (range_drawn(dbp, area_blocks(dpsm, dbw, BITS(trxpos, 48, 11) + h)) || + (dir == 2 && range_drawn(sbp, area_blocks(spsm, sbw, BITS(trxpos, 16, 11) + h))) || (dbg_drain_all & 2)) gs_drain(); + else if ((dbg_drain_all & 8) && nworkers && sring) { /* testing: a transfer over pages queued draws still write */ + u32 lo = q_head; for (int i = 0; i < nworkers; i++) { u32 t = q_tail_[i].v; if (q_head - t > q_head - lo) lo = t; } + u32 d0 = dbp / 32, d1 = (dbp + area_blocks(dpsm, dbw, BITS(trxpos, 48, 11) + h) - 1) / 32; + for (u32 q = lo; q != q_head; q++) { + const pcmd_t *S = &sring[cq[q % QN].st % SN]; + u32 f0 = S->fbp / 32, f1 = (S->fbp + area_blocks(S->fpsm, S->fbw, S->sy1 + 1) - 1) / 32; + if (f0 <= d1 && d0 <= f1) { uya_log("RACE: transfer dir %u to %04X (pages %u-%u) while prim %u draws FBP %04X (pages %u-%u), %u queued", dir, dbp, d0, d1, q, S->fbp, f0, f1, q_head - lo); gs_drain(); break; } + } + } + } + if (trxlog_on()) uya_log("TRX dir %u [%s]: %04X/%u/%X (%u,%u) -> %04X/%u/%X (%u,%u) %ux%u%s", dir, gs_src_int, (u32)BITS(bitbltbuf, 0, 14), (u32)BITS(bitbltbuf, 16, 6), (u32)BITS(bitbltbuf, 24, 6), + (u32)BITS(trxpos, 0, 11), (u32)BITS(trxpos, 16, 11), (u32)BITS(bitbltbuf, 32, 14), (u32)BITS(bitbltbuf, 48, 6), (u32)BITS(bitbltbuf, 56, 6), + (u32)BITS(trxpos, 32, 11), (u32)BITS(trxpos, 48, 11), (u32)BITS(trxreg, 0, 12), (u32)BITS(trxreg, 32, 12), trx_pend ? " (last one unfinished)" : ""); + if (gsgl_on && (dir == 0 || dir == 2)) { /* GL: what GPU targets drew there, and their copies of it */ + u32 w = BITS(trxreg, 0, 12), h = BITS(trxreg, 32, 12); + if (dir == 2) gsgl_cpu_read_rect(BITS(bitbltbuf, 24, 6), BITS(bitbltbuf, 0, 14), BITS(bitbltbuf, 16, 6), BITS(trxpos, 0, 11), BITS(trxpos, 16, 11), w, h); + gsgl_cpu_write_rect(BITS(bitbltbuf, 56, 6), BITS(bitbltbuf, 32, 14), BITS(bitbltbuf, 48, 6), BITS(trxpos, 32, 11), BITS(trxpos, 48, 11), w, h); + } + trx_active = 0; + if (trx_pend) { mark_blocks(trx_rbp, trx_rn); trx_pend = 0; } /* an unfinished upload */ + if (dir == 0 || dir == 2) { + u32 dbp = BITS(bitbltbuf, 32, 14), dbw = BITS(bitbltbuf, 48, 6), dpsm = BITS(bitbltbuf, 56, 6); + u32 dy = BITS(trxpos, 48, 11), h = BITS(trxreg, 32, 12); + trx_rbp = dbp; trx_rn = area_blocks(dpsm, dbw, dy + h); + if (dir == 2) mark_blocks(trx_rbp, trx_rn); + else { trx_pend = 1; trx_hash = 2166136261u; } + } + if (dir == 0) { trx_x = 0; trx_y = 0; trx_nacc = 0; trx_active = 1; } + else if (dir == 2) { /* local to local */ + u32 sbp = BITS(bitbltbuf, 0, 14), sbw = BITS(bitbltbuf, 16, 6), spsm = BITS(bitbltbuf, 24, 6); + u32 dbp = BITS(bitbltbuf, 32, 14), dbw = BITS(bitbltbuf, 48, 6), dpsm = BITS(bitbltbuf, 56, 6); + u32 sx = BITS(trxpos, 0, 11), sy = BITS(trxpos, 16, 11), dx = BITS(trxpos, 32, 11), dy = BITS(trxpos, 48, 11); + u32 w = BITS(trxreg, 0, 12), h = BITS(trxreg, 32, 12); + for (u32 y = 0; y < h; y++) + for (u32 x = 0; x < w; x++) + px_write(dpsm, dbp, dbw, (dx + x) & 2047, (dy + y) & 2047, px_read(spsm, sbp, sbw, (sx + x) & 2047, (sy + y) & 2047)); + } +} + +static void trx_pixel(u32 v) +{ + n_trx_px++; + u32 dbp = BITS(bitbltbuf, 32, 14), dbw = BITS(bitbltbuf, 48, 6), dpsm = BITS(bitbltbuf, 56, 6); + u32 dx = BITS(trxpos, 32, 11), dy = BITS(trxpos, 48, 11); + u32 w = BITS(trxreg, 0, 12), h = BITS(trxreg, 32, 12); + if (!w || trx_y >= h) { trx_active = 0; return; } + px_write(dpsm, dbp, dbw, (dx + trx_x) & 2047, (dy + trx_y) & 2047, v); + trx_hash = (trx_hash ^ v) * 16777619u; + if (++trx_x >= w) { trx_x = 0; if (++trx_y >= h) { trx_active = 0; if (trx_pend) trx_finish(); } } +} + +static void trx_data_(const u8 *d, u32 bytes); +static void trx_data(const u8 *d, u32 bytes) +{ + unsigned long long t_ = rdtsc_(); + trx_data_(d, bytes); + t_trx += rdtsc_() - t_; +} +static void trx_data_(const u8 *d, u32 bytes) +{ + u32 dpsm = BITS(bitbltbuf, 56, 6), bits = trx_bits(dpsm); + u32 i = 0; + /* whole runs of a row at a time for the common formats */ + if ((dpsm == 0x13 || dpsm == 0x00 || dpsm == 0x14) && !trx_nacc) { + u32 dbp = BITS(bitbltbuf, 32, 14), dbw = BITS(bitbltbuf, 48, 6); + u32 dx = BITS(trxpos, 32, 11), dy = BITS(trxpos, 48, 11); + u32 w = BITS(trxreg, 0, 12), h = BITS(trxreg, 32, 12); + while (trx_active && i < bytes) { + if (!w || trx_y >= h) { trx_active = 0; break; } + u32 avail = dpsm == 0x13 ? bytes - i : (dpsm == 0x00 ? (bytes - i) / 4 : (bytes - i) * 2); + u32 run = w - trx_x; + if (run > avail) run = avail; + if (dpsm == 0x14) run &= ~1u; + if (!run) break; + u32 y = (dy + trx_y) & 2047, hsh = trx_hash; + /* a swizzle column group (16, 8 or 32 pixels: the same block) at a time */ + if (dpsm == 0x13) { + const u8 *col = gs_col8[y & 15]; + for (u32 k = 0; k < run; ) { + u32 X = (dx + trx_x + k) & 2047, n = 16 - (X & 15); + if (n > run - k) n = run - k; + u32 base = a8(dbp, dbw, X & ~15u, y) - col[0]; + for (u32 j = 0; j < n; j++) { u32 v = d[i + k + j]; vram8()[base + col[(X & 15) + j]] = (u8)v; hsh = (hsh ^ v) * 16777619u; } + k += n; + } + i += run; + } else if (dpsm == 0x00) { + const u8 *col = gs_col32[y & 7]; + for (u32 k = 0; k < run; ) { + u32 X = (dx + trx_x + k) & 2047, n = 8 - (X & 7); + if (n > run - k) n = run - k; + u32 base = a32(dbp, dbw, X & ~7u, y) - col[0]; + for (u32 j = 0; j < n; j++) { u32 v; memcpy(&v, d + i + 4 * (k + j), 4); vram[base + col[(X & 7) + j]] = v; hsh = (hsh ^ v) * 16777619u; } + k += n; + } + i += run * 4; + } else { + const u16 *col = gs_col4[y & 15]; + for (u32 k = 0; k < run; ) { + u32 X = (dx + trx_x + k) & 2047, n = 32 - (X & 31); + if (n > run - k) n = run - k; + u32 base = a4(dbp, dbw, X & ~31u, y) - col[0]; + for (u32 j = 0; j < n; j++) { + u32 kk = k + j, v = (d[i + kk / 2] >> ((kk & 1) * 4)) & 15, nb = base + col[(X & 31) + j]; + vram8()[nb >> 1] = (u8)((vram8()[nb >> 1] & ((nb & 1) ? 0x0F : 0xF0)) | (v << ((nb & 1) * 4))); + hsh = (hsh ^ v) * 16777619u; + } + k += n; + } + i += run / 2; + } + trx_hash = hsh; n_trx_px += run; + trx_x += run; + if (trx_x >= w) { trx_x = 0; if (++trx_y >= h) { trx_active = 0; if (trx_pend) trx_finish(); } } + } + } + for (; i < bytes && trx_active; i++) { + if (bits == 4) { trx_pixel(d[i] & 15); if (trx_active) trx_pixel(d[i] >> 4); continue; } + if (bits == 8) { trx_pixel(d[i]); continue; } + trx_acc[trx_nacc++] = d[i]; + u32 need = bits / 8; + if (trx_nacc == need) { + u32 v = trx_acc[0] | (trx_acc[1] << 8) | (need > 2 ? (u32)trx_acc[2] << 16 : 0) | (need > 3 ? (u32)trx_acc[3] << 24 : 0); + trx_pixel(v); + trx_nacc = 0; + } + } +} + +static void gs_bad(const char *what, u64 v); +/* ---- register writes ---- */ +void uya_gs_write(u32 addr, u64 v) +{ +#ifdef GS_WRITE_HOOK + GS_WRITE_HOOK(addr, v); +#endif + { /* testing: UYA_FINDREG=reg,value (hex): where the GIF data that writes it lies (with UYA_MTGS=0), once per vsync */ + static int on = -1; static unsigned reg, lastv; static u64 want; + if (on < 0) { const char *e = getenv("UYA_FINDREG"); on = e && sscanf(e, "%x,%llx", ®, (unsigned long long *)&want) == 2; } + if (on && addr == reg && v == want && lastv != uya_frame_count()) { + lastv = uya_frame_count(); + uya_log("FINDREG %02X = %016llX at vsync %u from %s, data at %08X", reg, (unsigned long long)v, lastv, gs_src_int, (u32)(uintptr_t)gif_cur_qw); + if (!mtgs && gif_cur_qw) for (int j = -24; j < 8; j++) { const u32 *w = gif_cur_qw + j * 4; uya_log(" %08X: %08X %08X %08X %08X", (u32)(uintptr_t)w, w[0], w[1], w[2], w[3]); } + } + } + if (!(addr <= 0x05 && addr) && addr != 0x0A && addr != 0x0C && addr != 0x0D && (addr < 0x50 || addr > 0x54)) st_dirty = 1; + switch (addr) { + case 0x00: prim = v; prim_reset(); break; + case 0x01: rgbaq = v; { u32 q = (u32)(v >> 32); memcpy(&qreg, &q, 4); } break; + case 0x02: st = v; break; + case 0x03: uv = v; break; + case 0x04: vertex_kick(BITS(v, 0, 16), BITS(v, 16, 16), BITS(v, 32, 24), (u8)BITS(v, 56, 8), 1); break; + case 0x05: vertex_kick(BITS(v, 0, 16), BITS(v, 16, 16), (u32)(v >> 32), 0, 1); break; + case 0x0C: vertex_kick(BITS(v, 0, 16), BITS(v, 16, 16), BITS(v, 32, 24), (u8)BITS(v, 56, 8), 0); break; + case 0x0D: vertex_kick(BITS(v, 0, 16), BITS(v, 16, 16), (u32)(v >> 32), 0, 0); break; + case 0x06: case 0x07: + { + static int left = -1; /* testing: UYA_BADTEX=n logs TEX0 writes whose top half looks like a pointer, with where the data came from */ + if (left < 0) { const char *e = getenv("UYA_BADTEX"); left = e ? atoi(e) : 0; } + if (left > 0 && (v >> 48) >= 0xFF00) { + left--; + uya_log("odd TEX0 %016llX from %s, data at %08X", (unsigned long long)v, gs_src_int, (u32)(uintptr_t)gif_cur_qw); + for (int j = -12; j < 8; j++) { const u32 *w = gif_cur_qw + j * 4; uya_log(" %08X: %08X %08X %08X %08X", (u32)(uintptr_t)w, w[0], w[1], w[2], w[3]); } + } + } + ctx[addr - 6].tex0 = v; clut_load(v); break; + case 0x08: case 0x09: ctx[addr - 8].clamp = v; break; + case 0x0A: fog = v; break; + case 0x14: case 0x15: ctx[addr - 0x14].tex1 = v; break; + case 0x16: case 0x17: { + gs_ctx_t *c = &ctx[addr - 0x16]; + u64 m = (0x3Full << 20) | (~0ull << 37); /* PSM and the CLUT fields come from TEX2 */ + c->tex0 = (c->tex0 & ~m) | (v & m); + clut_load(c->tex0); + } break; + case 0x18: case 0x19: ctx[addr - 0x18].xyoffset = v; break; + case 0x34: case 0x35: ctx[addr - 0x34].miptbp1 = v; break; + case 0x36: case 0x37: ctx[addr - 0x36].miptbp2 = v; break; + case 0x1A: prmodecont = v & 1; break; + case 0x1B: prmode = v; break; + case 0x1C: texclut = v; break; + case 0x3B: texa = v; break; + case 0x3D: fogcol = v; break; + case 0x40: case 0x41: ctx[addr - 0x40].scissor = v; break; + case 0x42: case 0x43: ctx[addr - 0x42].alpha = v; break; + case 0x45: dthe = v; break; + case 0x46: colclamp = v; break; + case 0x47: case 0x48: ctx[addr - 0x47].test = v; break; + case 0x49: pabe = v; break; + case 0x4A: case 0x4B: ctx[addr - 0x4A].fba = v; break; + case 0x4C: case 0x4D: { + static int nlog = -1; + if (nlog < 0) { const char *e = getenv("UYA_FRAMELOG"); nlog = e ? atoi(e) : 0; } + if (nlog > 0 && ctx[addr - 0x4C].frame != v) { nlog--; uya_log("GS FRAME_%u = FBP 0x%X FBW %u PSM 0x%X MSK %08X", addr - 0x4C + 1, BITS(v, 0, 9) * 32, BITS(v, 16, 6) * 64, BITS(v, 24, 6), (u32)(v >> 32)); } + { u32 ps = BITS(v, 24, 6); if (!BITS(v, 16, 6) || !(ps == 0 || ps == 1 || ps == 2 || ps == 0xA)) gs_bad("FRAME", v); } + ctx[addr - 0x4C].frame = v; + } break; + case 0x4E: case 0x4F: ctx[addr - 0x4E].zbuf = v; break; + case 0x50: bitbltbuf = v; break; + case 0x51: trxpos = v; break; + case 0x52: trxreg = v; break; + case 0x53: trx_start(BITS(v, 0, 2)); break; + case 0x54: { u8 b[8]; memcpy(b, &v, 8); if (trx_active) trx_data(b, 8); } break; + default: break; + } +} + +/* ---- GIF ---- */ +/* Each GIF path (PATH1 XGKICK, PATH2 VIF1 DIRECT, PATH3 GIF DMA/FIFO) has its + * own tag in progress: the GIF only switches paths between packets. */ +typedef struct { u32 nloop, nreg, flg, eop, r; u64 regs; int active; } giftag_t; +static giftag_t gif_tags[3]; +/* testing: who sent the GIF data (set by the callers), and the last tags */ +const char *uya_gs_src = "?"; +static struct { const char *src; u64 lo, hi; } tag_hist[16]; +static u32 tag_hist_n; +static void gs_bad(const char *what, u64 v) +{ + static int left = 3; + if (left <= 0) return; + left--; + uya_log("GS: implausible %s %016llX (from %s) at vsync %u; last GIF tags:", what, (unsigned long long)v, gs_src_int, uya_frame_count()); + for (u32 i = 0; i < 16; i++) { + u32 k = (tag_hist_n + i) & 15; + if (tag_hist[k].src) uya_log(" %-28s %016llX %016llX", tag_hist[k].src, (unsigned long long)tag_hist[k].lo, (unsigned long long)tag_hist[k].hi); + } + if (gs_src_int[4] == '1' && !mtgs) { void uya_vu_dump_kick(void); uya_vu_dump_kick(); } +} + + +/* ---- testing: recording the GS input ---- + * UYA_GSREC=file,from,n (or F9 in the game window) saves the GS state at vsync `from` + * (registers and memory) and then every GIF packet, movie frame and vsync of + * the next n vsyncs, so tools/gs_replay.c can run the same input through + * another build of this file and compare the results (VRAM after each vsync). */ +#define GS_SNAP_FIELDS(X) X(ctx) X(prim) X(prmode) X(prmodecont) X(rgbaq) X(st) X(uv) X(fog) X(texa) X(fogcol) X(colclamp) \ + X(pabe) X(dthe) X(bitbltbuf) X(trxpos) X(trxreg) X(texclut) X(qreg) X(clut) X(clut_hash) X(cbp0) X(cbp1) X(vq) X(vcount) \ + X(gif_tags) X(trx_x) X(trx_y) X(trx_active) X(trx_acc) X(trx_nacc) +static FILE *rec_f; +static long rec_vs, rec_from = -1, rec_to; +static volatile int rec_hotkey; +/* F9 (platform_win32.c): record the next UYA_GSREC_N vsyncs (default 150) to gsrec_NN.bin next to the exe */ +void uya_gs_rec_hotkey(void) { rec_hotkey = 1; } +static void rec_put(const void *p, size_t n) { if (rec_f) fwrite(p, 1, n, rec_f); } +static void rec_vsync(const u64 *r) +{ + if (rec_from == -1) { + const char *e = getenv("UYA_GSREC"); + rec_from = -2; + if (e) { char path[256]; long n = 0; if (sscanf(e, "%255[^,],%ld,%ld", path, &rec_from, &n) == 3) { rec_to = rec_from + n; rec_f = fopen(path, "wb"); if (!rec_f) rec_from = -2; } else rec_from = -2; } + } + if (rec_hotkey && !rec_f) { + static int nrec; + char path[600]; const char *ne = getenv("UYA_GSREC_N"); long n = ne ? atol(ne) : 150; + rec_hotkey = 0; + if (n < 1) n = 150; + snprintf(path, sizeof path, "%s/gsrec_%02d.bin", plat_exe_dir(), nrec++); + rec_f = fopen(path, "wb"); + if (rec_f) { rec_from = rec_vs; rec_to = rec_vs + n; uya_log("GS input recording to %s (%ld vsyncs)", path, n); } + } + rec_hotkey = 0; + if (!rec_f) { rec_vs++; return; } + if (rec_vs == rec_from) { + u32 m = 0x52534755u; rec_put(&m, 4); + if (gsgl_on) gsgl_cpu_read(0, 0x4000); +#define X(f) { u32 n_ = sizeof f; rec_put(&n_, 4); rec_put(&f, sizeof f); } + GS_SNAP_FIELDS(X) +#undef X + rec_put(vram, 4 << 20); + } else if (rec_vs > rec_from) { + u32 k = 2; rec_put(&k, 4); rec_put(r, 48); + if (rec_vs >= rec_to) { fclose(rec_f); rec_f = 0; uya_log("GS input recorded"); } + } + rec_vs++; +} +static void rec_gif(const u32 *d, u32 qwc, u32 path) +{ + if (!rec_f || rec_vs <= rec_from) return; + u32 h[3] = { 1, path, qwc }; rec_put(h, 12); rec_put(d, qwc * 16); +} +static void uya_gs_gif_(const u32 *d, u32 qwc); +/* ---- the GS thread ---- + * With UYA_MTGS (on by default with 3 or more cores) GIF data is copied into + * a FIFO and the GS (register writes, primitives, transfers, the texture + * cache) runs on its own thread, like the real GS beside the EE. The EE side + * waits for it at each vsync before the frame is shown (gs_sync). */ +#define GF_WORDS (1u << 22) +static u32 *gfifo; +static line_u32 gf_head_, gf_tail_; /* written by the game's thread and the GS thread */ +#define gf_head (gf_head_.v) +#define gf_tail (gf_tail_.v) +static const char *path_name[3] = { "PATH1 (XGKICK)", "PATH2 (VIF1 DIRECT)", "PATH3" }; + +static park_t gspark; +static void gs_vsync_marker(const u32 *e); +static void gs_movie_marker(const u32 *e); +static void gl_mem_do(const u32 *e); +static void gs_thread_loop(void) +{ + int spin = 0; + if (gsgl_on && !gsgl_threaded()) plat_gl_current(1); /* the GL context lives on this thread (unless on the GL thread) */ + for (;;) { + u32 h = __atomic_load_n(&gf_head, __ATOMIC_ACQUIRE), t = gf_tail; + if (t == h) { + if (spin++ < 2000) { __asm__ volatile("pause"); continue; } + __atomic_store_n(&gspark.sleeping, 1, __ATOMIC_SEQ_CST); + if (__atomic_load_n(&gf_head, __ATOMIC_SEQ_CST) == t) park_wait(&gspark); + __atomic_store_n(&gspark.sleeping, 0, __ATOMIC_RELEASE); + continue; + } + spin = 0; + unsigned long long b0_ = rdtsc_(); + while (t != h) { + const u32 *e = gfifo + (t % GF_WORDS); + u32 qwc = e[0], path = e[1]; + if (path == 0xFFFFFFFFu) { t += GF_WORDS - (t % GF_WORDS); continue; } /* wrap to the start */ + if (path == 0xFFFFFFFEu) { gs_vsync_marker(e + 4); t += 4 + qwc * 4; continue; } /* a vsync: capture the frame */ + if (path == 0xFFFFFFFCu) { gs_movie_marker(e + 4); t += 4 + qwc * 4; continue; } /* an FMV frame to show */ + if (path == 0xFFFFFFFDu) { gl_mem_do(e + 4); t += 4 + qwc * 4; continue; } /* GL: the game's thread touches GS memory */ + gs_src_int = path_name[path]; + if (__builtin_expect(rec_f != 0, 0)) rec_gif(e + 4, qwc, path); + uya_gs_gif_(e + 4, qwc); + t += 4 + qwc * 4; + } + gs_thread_busy += rdtsc_() - b0_; + __atomic_store_n(&gf_tail, t, __ATOMIC_RELEASE); + } +} +#ifdef _WIN32 +static DWORD WINAPI gs_thread_main(LPVOID a) { (void)a; gs_thread_loop(); return 0; } +#else +static void *gs_thread_main(void *a) { (void)a; gs_thread_loop(); return 0; } +#endif +static void mtgs_init(int cores) +{ + const char *e = getenv("UYA_MTGS"); + mtgs = e ? atoi(e) : cores >= 3; + if (!mtgs) return; + gfifo = malloc(GF_WORDS * 4); + park_init(&gspark); +#ifdef _WIN32 + CreateThread(0, 0, gs_thread_main, 0, 0, 0); +#else + pthread_t th; pthread_create(&th, 0, gs_thread_main, 0); pthread_detach(th); +#endif + uya_log("GS: on its own thread"); +} +/* wait until the GS thread has done everything sent to it */ +static void gs_sync(void) +{ + if (!mtgs) return; + int spin = 0; + while (__atomic_load_n(&gf_tail, __ATOMIC_ACQUIRE) != gf_head) cpu_relax(spin++); +} + +static void gs_fifo_put(const void *data, u32 qwc, u32 path) +{ + const u32 *d = data; + while (qwc) { + u32 n = qwc > 0x10000 ? 0x10000 : qwc, need = 4 + n * 4, h = gf_head, spin = 0; + if ((h % GF_WORDS) + need > GF_WORDS) { /* no room before the end: a wrap marker */ + while (h + (GF_WORDS - h % GF_WORDS) - __atomic_load_n(&gf_tail, __ATOMIC_ACQUIRE) > GF_WORDS - 4) cpu_relax(spin++); + gfifo[h % GF_WORDS] = 0; gfifo[h % GF_WORDS + 1] = 0xFFFFFFFFu; + h += GF_WORDS - h % GF_WORDS; + __atomic_store_n(&gf_head, h, __ATOMIC_RELEASE); + } + while (h + need - __atomic_load_n(&gf_tail, __ATOMIC_ACQUIRE) > GF_WORDS - 4) cpu_relax(spin++); + u32 *w = gfifo + h % GF_WORDS; + w[0] = n; w[1] = path; w[2] = w[3] = 0; + memcpy(w + 4, d, n * 16); + __atomic_store_n(&gf_head, h + need, __ATOMIC_SEQ_CST); + park_wake(&gspark); + d += n * 4; qwc -= n; + } +} + +void uya_gs_gif(const u32 *d, u32 qwc) +{ + unsigned long long t = timing() ? rdtsc_() : 0; + if (__builtin_expect(rec_f != 0, 0) && !mtgs) rec_gif(d, qwc, (uya_gs_src[0] == 'P' && uya_gs_src[4] >= '1' && uya_gs_src[4] <= '3') ? (u32)(uya_gs_src[4] - '1') : 2); + if (!mtgs) { gs_src_int = uya_gs_src; uya_gs_gif_(d, qwc); } + else gs_fifo_put(d, qwc, (uya_gs_src[0] == 'P' && uya_gs_src[4] >= '1' && uya_gs_src[4] <= '3') ? (u32)(uya_gs_src[4] - '1') : 2); + if (uya_timing) uya_t_gs += rdtsc_() - t; +} +static void uya_gs_gif_(const u32 *d, u32 qwc) +{ + u32 q = 0; + if (!vram) return; + int path = (gs_src_int[0] == 'P' && gs_src_int[4] >= '1' && gs_src_int[4] <= '3') ? gs_src_int[4] - '1' : 2; + giftag_t *tp = &gif_tags[path]; +#define tag (*tp) + if (path == 0) tag.active = 0; /* XGKICK starts a packet at its address */ + while (q < qwc) { + const u32 *t = d + q * 4; gif_cur_qw = t; + if (!tag.active) { + u64 lo = ((u64)t[1] << 32) | t[0], hi = ((u64)t[3] << 32) | t[2]; + tag.nloop = (u32)(lo & 0x7FFF); tag.eop = BITS(lo, 15, 1); tag.flg = BITS(lo, 58, 2); + tag.nreg = BITS(lo, 60, 4); if (!tag.nreg) tag.nreg = 16; + tag.regs = hi; tag.r = 0; + tag_hist[tag_hist_n & 15].src = gs_src_int; tag_hist[tag_hist_n & 15].lo = lo; tag_hist[tag_hist_n & 15].hi = hi; tag_hist_n++; + { + static int nlog = -1; + static unsigned nfrom; + if (nlog < 0) { const char *e = getenv("UYA_GSLOG"); nlog = e ? atoi(e) : 0; e = getenv("UYA_GSLOG_FROM"); nfrom = e ? (unsigned)atoi(e) : 0; } /* testing: UYA_GSLOG=n tags from vsync UYA_GSLOG_FROM */ + if (nlog > 0 && uya_frame_count() >= nfrom) { nlog--; uya_log("GIF tag [%s, %u qw]: nloop %u eop %u pre %u prim 0x%X flg %u nreg %u regs %016llX", gs_src_int, qwc - q, tag.nloop, tag.eop, BITS(lo, 46, 1), BITS(lo, 47, 11), tag.flg, tag.nreg, (unsigned long long)hi); } + } + if (path == 1) { /* testing: UYA_P2LOG=1 logs PATH2 tags that look like data, with the PATH2 tags before */ + static int p2on = -1, p2n = 30; static u32 hn; static struct { u32 v, q, n; u64 lo, hi; } h[12]; + if (p2on < 0) p2on = getenv("UYA_P2LOG") != 0; + if (p2on) { + if (((lo >> 16) & 0x3FFFFFFFull) && p2n > 0) { + p2n--; + uya_log("P2 bad tag v%u q %u/%u: %016llX %016llX; earlier PATH2 tags:", uya_frame_count(), q, qwc, (unsigned long long)lo, (unsigned long long)hi); + for (u32 k = 0; k < 12; k++) { u32 x = (hn + k) % 12; if (h[x].n) uya_log(" v%u q %u/%u: %016llX %016llX", h[x].v, h[x].q, h[x].n, (unsigned long long)h[x].lo, (unsigned long long)h[x].hi); } + if (p2n >= 28) { void uya_vif1_dump_hist(void); uya_vif1_dump_hist(); } + } + h[hn % 12].v = uya_frame_count(); h[hn % 12].q = q; h[hn % 12].n = qwc; h[hn % 12].lo = lo; h[hn % 12].hi = hi; hn++; + } + } + if (BITS(lo, 46, 1)) uya_gs_write(0x00, BITS(lo, 47, 11)); + qreg = 1.0f; + q++; + tag.active = tag.nloop > 0; + continue; + } + if (tag.flg == 0) { /* PACKED */ + u32 rg = (u32)(tag.regs >> (tag.r * 4)) & 15; + u64 lo = ((u64)t[1] << 32) | t[0], hi = ((u64)t[3] << 32) | t[2]; + switch (rg) { + case 0x0: uya_gs_write(0x00, lo); break; + case 0x1: rgbaq = (t[0] & 255) | ((t[1] & 255) << 8) | ((t[2] & 255) << 16) | ((u64)(t[3] & 255) << 24) | ((u64)0 << 32); break; + case 0x2: st = lo; memcpy(&qreg, &t[2], 4); break; + case 0x3: uv = (t[0] & 0x3FFF) | ((u64)(t[1] & 0x3FFF) << 16); break; + case 0x4: vertex_kick(t[0] & 0xFFFF, t[1] & 0xFFFF, (t[2] >> 4) & 0xFFFFFF, (u8)(t[3] >> 4), !((t[3] >> 15) & 1)); break; + case 0x5: vertex_kick(t[0] & 0xFFFF, t[1] & 0xFFFF, t[2], 0, !((t[3] >> 15) & 1)); break; + case 0xA: fog = (u64)((t[3] >> 4) & 255) << 56; break; + case 0xC: vertex_kick(t[0] & 0xFFFF, t[1] & 0xFFFF, (t[2] >> 4) & 0xFFFFFF, (u8)(t[3] >> 4), 0); break; + case 0xD: vertex_kick(t[0] & 0xFFFF, t[1] & 0xFFFF, t[2], 0, 0); break; + case 0xE: uya_gs_write((u32)(hi & 0xFF), lo); break; + case 0xF: break; + default: uya_gs_write(rg, lo); break; /* TEX0_x, CLAMP_x: same as A+D at that address */ + } + q++; + if (++tag.r >= tag.nreg) { tag.r = 0; if (--tag.nloop == 0) tag.active = 0; } + } else if (tag.flg == 1) { /* REGLIST: two 64-bit registers per qword */ + for (int h = 0; h < 2 && tag.active; h++) { + u64 val = ((u64)t[h * 2 + 1] << 32) | t[h * 2]; + u32 rg = (u32)(tag.regs >> (tag.r * 4)) & 15; + if (rg != 0xE && rg != 0xF) uya_gs_write(rg == 0x1 ? 0x01 : rg, val); + if (++tag.r >= tag.nreg) { tag.r = 0; if (--tag.nloop == 0) tag.active = 0; } + } + q++; + } else { /* IMAGE: all of the packet's data here at once */ + u32 n = qwc - q < tag.nloop ? qwc - q : tag.nloop; + if (trx_active) trx_data((const u8 *)t, n * 16); + q += n; + if ((tag.nloop -= n) == 0) tag.active = 0; + } + } +#undef tag +} + +/* ---- init and display ---- */ +/* UYA_RENDERER=gl|soft (or renderer= in rac1_wrapper.cfg): the GPU draws + * (default where there is GL: Windows) or the software rasterizer; GL falls + * back to software if it cannot start */ +const char *uya_renderer_cfg; +void uya_gs_init(void) +{ + if (vram) return; + gs_swizzle_init(); + timing(); + vram = calloc(1, 4 << 20); + { + const char *r = getenv("UYA_RENDERER"); + if (!r) r = uya_renderer_cfg; +#ifdef _WIN32 + if (!r) r = "gl"; +#else + if (!r) r = "soft"; +#endif + if (!strcmp(r, "gl")) { + if (plat_gl_init() && gsgl_init(vram)) { plat_gl_active = 1; uya_log("GS: drawing with OpenGL"); } + else uya_log("GS: OpenGL not available, drawing in software"); + } + } + if (gsgl_on && !gsgl_threaded()) plat_gl_current(0); /* free for the GS thread (MTGS) to take */ + gs_threads_init(); + if (gsgl_on && !mtgs && !gsgl_threaded()) plat_gl_current(1); +} + +/* GL with MTGS: the game's thread asks the GS thread to sync GS memory with + * the GPU targets before it reads (StoreImage) or writes (movie frames) it */ +static void gl_mem_do(const u32 *e) +{ + if (e[0]) gsgl_cpu_write_rect(e[1], e[2], e[3], e[4], e[5], e[6], e[7]); + else gsgl_cpu_read_rect(e[1], e[2], e[3], e[4], e[5], e[6], e[7]); +} +static void gs_sync(void); +static void gl_mem_req(int write, u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h) +{ + if (!gsgl_on) return; + u32 e[8] = { (u32)write, psm, bp, bw, x, y, w, h }; + if (!mtgs) { gl_mem_do(e); return; } + gs_fifo_put(e, 2, 0xFFFFFFFDu); + gs_sync(); +} + +static u32 frame_px[1024 * 1024]; + +/* what the display circuits show (PMODE, SMODE2, DISPFB, DISPLAY); 0 if nothing */ +static int gs_disp(u64 pmode, u64 smode2, u64 dispfb_1, u64 display_1, u64 dispfb_2, u64 display_2, gs_disp_t *d) +{ + static int logged; + int circ = (pmode & 1) ? 1 : ((pmode & 2) ? 2 : 0); + if (!circ) return 0; + u64 dispfb = circ == 1 ? dispfb_1 : dispfb_2; + u64 display = circ == 1 ? display_1 : display_2; + u32 fbp = BITS(dispfb, 0, 9) * 32, fbw = BITS(dispfb, 9, 6), psm = BITS(dispfb, 15, 5); + u32 dbx = BITS(dispfb, 32, 11), dby = BITS(dispfb, 43, 11); + u32 magh = BITS(display, 23, 4) + 1, magv = BITS(display, 27, 2) + 1; + u32 dw = BITS(display, 32, 12) + 1, dh = BITS(display, 44, 11) + 1; + u32 w = dw / magh, h = dh / magv; + if ((smode2 & 3) == 3) h /= 2; /* interlaced, frame mode: each field shows half the lines */ + if (w > 1024) w = 1024; + if (h > 1024) h = 1024; + static u64 last_disp, last_fb; + if (display != last_disp || dispfb != last_fb) { logged = 0; last_disp = display; last_fb = dispfb; } + if (!logged) { + uya_log("GS display: circuit %d, FBP 0x%X FBW %u PSM 0x%X, %ux%u (DW %u DH %u MAGH %u MAGV %u, SMODE2 %llu)", + circ, fbp, fbw * 64, psm, w, h, dw, dh, magh, magv, (unsigned long long)smode2); + logged = 1; + } + if (!w || !h || !fbw) return 0; + /* Adjust Screen moves the picture by changing DISPLAY's DX/DY. The first + * position seen in each video mode is the centre; later changes shift the + * picture in the window by the same amount (DX counts in MAGH units). */ + static u64 base_key; static s32 base_dx, base_dy; + s32 cdx = (s32)BITS(display, 0, 12), cdy = (s32)BITS(display, 12, 11); + u64 key = (display >> 23) | (smode2 << 60); + if (key != base_key) { base_key = key; base_dx = cdx; base_dy = cdy; } + s32 shx = (cdx - base_dx) / (s32)magh, shy = cdy - base_dy; + if ((smode2 & 1) && !((smode2 >> 1) & 1)) shy *= 2; /* interlaced field mode: DY counts field lines */ + d->fbp = fbp; d->fbw = fbw; d->psm = psm; d->dbx = dbx; d->dby = dby; d->w = w; d->h = h; d->shx = shx; d->shy = shy; + return 1; +} +static void fps_log(void); +/* the frame the display circuits show, from GS memory into out (BGRX); 0 if nothing is shown */ +static int gs_capture(u64 pmode, u64 smode2, u64 dispfb_1, u64 display_1, u64 dispfb_2, u64 display_2, u32 *out, u32 *ow, u32 *oh) +{ + gs_disp_t d; + if (!gs_disp(pmode, smode2, dispfb_1, display_1, dispfb_2, display_2, &d)) { fps_log(); return 0; } + u32 fbp = d.fbp, fbw = d.fbw, psm = d.psm, dbx = d.dbx, dby = d.dby, w = d.w, h = d.h; + s32 shx = d.shx, shy = d.shy; + const int p32 = !(psm == 0x02 || psm == 0x0A); + for (u32 y = 0; y < h; y++) + for (u32 x = 0; x < w; x++) { + s32 fx = (s32)x - shx, fy = (s32)y - shy; + if (fx < 0 || fy < 0 || fx >= (s32)w || fy >= (s32)h) { out[y * w + x] = 0; continue; } + u32 v = p32 ? vram[a32(fbp, fbw, (dbx + (u32)fx) & 2047, (dby + (u32)fy) & 2047)] /* px_read for PSMCT32/24 */ + : px_read(psm, fbp, fbw, (dbx + (u32)fx) & 2047, (dby + (u32)fy) & 2047), c; + if (psm == 0x02 || psm == 0x0A) c = ((v & 31) << 19) | (((v >> 5) & 31) << 11) | (((v >> 10) & 31) << 3); + else c = ((v & 255) << 16) | (v & 0xFF00) | ((v >> 16) & 255); /* RGBA in memory to BGRX for the window */ + out[y * w + x] = c; + } + fps_log(); + *ow = w; *oh = h; + return 1; +} +/* UYA_FPSLOG: speed and GS statistics every 100 vsyncs; UYA_VRAMDUMP=n: GS memory to vram.bin at vsync n */ +static void fps_log(void) +{ + { + static long vn; static long want = -2; + if (want == -2) { const char *e = getenv("UYA_VRAMDUMP"); want = e ? atol(e) : -1; } + if (vn % 100 == 0) { /* VU results the VU would have kept finite (vu_fc, vu.h): a few lines, for bug reports */ + extern u32 vu_nonfinite; static u32 nf0, nlines; + u32 nf = vu_nonfinite; + if (nf != nf0 && nlines < 40) { nlines++; uya_log("VU: %u results overflowed to inf/NaN by vsync %ld (made +-MAX, as on the PS2)", nf - nf0, vn); } + nf0 = nf; + } + if (getenv("UYA_FPSLOG") && vn % 100 == 0) { + static struct timespec t0; struct timespec t1; clock_gettime(CLOCK_MONOTONIC, &t1); + extern unsigned long long uya_t_vu; static unsigned long long c0, g0, v0; unsigned long long c1 = rdtsc_(); + if (vn) uya_log("vsync %ld: %.1f vsyncs/s; GS %.0f%%, VU %.0f%% (VU includes its GS)", vn, 100.0 / ((t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9), + 100.0 * (uya_t_gs - g0) / (c1 - c0), 100.0 * (uya_t_vu - v0) / (c1 - c0)); + static unsigned long long p0, x0; static unsigned long long tp0; + extern unsigned long long uya_tex_px; + extern unsigned long long nsends; static unsigned long long s0; + uya_log(" %llu DMA sends (frames) in 100 vsyncs; per vsync: %llu waits for the GS threads (%u for layouts), %llu textures decoded, %llu pixels uploaded", nsends - s0, n_drain_wait / 100, n_layout_drain / 100, n_tc_build / 100, n_trx_px / 100); s0 = nsends; + uya_log(" texture rebuilds for stale data %llu, reused by content %llu; marks: uploads %llu, targets %llu", n_tc_stale / 100, n_tc_reuse / 100, n_mark_trx / 100, n_mark_draw / 100); + n_tc_stale = n_tc_reuse = n_mark_trx = n_mark_draw = 0; + uya_log(" time: drawing %.0f%%, texture decode %.0f%%, uploads %.0f%%", 100.0 * t_draw / (c1 - c0), 100.0 * t_tcb / (c1 - c0), 100.0 * t_trx / (c1 - c0)); + if (mtgs || gsgl_on) { /* where a frame's time goes, per thread (ms per vsync) */ + double ms = (double)((t1.tv_sec - t0.tv_sec) * 1000.0 + (t1.tv_nsec - t0.tv_nsec) / 1e6) / 100.0, busy = 0, wait = 0; + if (gsgl_on) gsgl_thread_times(&busy, &wait); + uya_log(" threads (ms per vsync, %.1f between vsyncs): GS thread busy %.1f (%.1f of it waiting for the GL thread), GL thread busy %.1f%s", + ms, mtgs ? ms * (double)gs_thread_busy / (double)(c1 - c0) : 0.0, wait / 100.0, busy / 100.0, gsgl_threaded() ? "" : " (no GL thread)"); + gs_thread_busy = 0; + } + n_drain_wait = n_tc_build = n_trx_px = 0; n_layout_drain = 0; t_draw = t_tcb = t_trx = 0; + uya_log(" per vsync: %llu prims, %llu pixels written, %llu textured pixels", (stat_prims - p0) / 100, (stat_pixels - x0) / 100, (uya_tex_px - tp0) / 100); + p0 = stat_prims; x0 = stat_pixels; tp0 = uya_tex_px; + c0 = c1; g0 = uya_t_gs; v0 = uya_t_vu; + t0 = t1; + } + if (++vn == want) { if (gsgl_on) gsgl_cpu_read(0, 0x4000); FILE *f = fopen("vram.bin", "wb"); if (f) { fwrite(vram, 4, 1 << 20, f); fclose(f); } uya_log("VRAM saved at vsync %ld", vn); } + } +} +/* GL: the display drawn into the window */ +static void gl_present(const u64 *r) +{ + gs_disp_t d; + int ok = gs_disp(r[0], r[1], r[2], r[3], r[4], r[5], &d); + fps_log(); + gsgl_present(ok ? &d : 0); +} + + +/* With the GS on its own thread the frame is captured there when the GS gets + * to the vsync (a marker in its FIFO), and the game goes on at once: it shows + * the last captured frame, so the GS works on frame N while the game prepares + * frame N+1 (at most two vsyncs ahead). */ +static u32 cap_buf[3][1024 * 1024], cap_w[3], cap_h[3], cap_ok[3]; +static volatile u32 vs_sent, vs_done; +static void gs_vsync_marker(const u32 *e) +{ + u64 r[6]; memcpy(r, e, sizeof r); + if (rec_f || rec_hotkey) gs_drain(); + rec_vsync(r); + if (gsgl_on) { gl_present(r); __atomic_store_n(&vs_done, vs_done + 1, __ATOMIC_RELEASE); return; } + gs_drain(); + u32 k = vs_done % 3; + cap_ok[k] = gs_capture(r[0], r[1], r[2], r[3], r[4], r[5], cap_buf[k], &cap_w[k], &cap_h[k]); + __atomic_store_n(&vs_done, vs_done + 1, __ATOMIC_RELEASE); +} + +/* FMV frames in GL mode: the GL context belongs to the GS (thread), so a + * decoded frame goes there to be drawn into the window. Two buffers; the + * player waits until the older one has been shown. */ +static u32 mv_buf[2][1024 * 1024]; +static volatile u32 mv_sent, mv_done; +static void gs_movie_marker(const u32 *e) +{ + u32 k = e[0] & 1; + gsgl_present_bgra(mv_buf[k], (int)e[1], (int)e[2]); + __atomic_store_n(&mv_done, mv_done + 1, __ATOMIC_RELEASE); +} +/* 1 if the GL renderer showed the frame (else the caller presents it itself) */ +int uya_gs_movie_frame(const u32 *px, int w, int h) +{ + if (!gsgl_on || !vram || w <= 0 || h <= 0 || w > 1024 || h > 1024) return 0; + if (!mtgs) { gsgl_present_bgra(px, w, h); return 1; } + int spin = 0; + while (mv_sent - __atomic_load_n(&mv_done, __ATOMIC_ACQUIRE) >= 2) cpu_relax(spin++); + u32 k = mv_sent & 1; + memcpy(mv_buf[k], px, (size_t)w * h * 4); + u32 m[12] = { k, (u32)w, (u32)h }; + mv_sent++; + gs_fifo_put(m, 3, 0xFFFFFFFCu); /* its own marker (0xFFFFFFFD is the GL memory sync) */ + return 1; +} + +void uya_gs_vsync(void) +{ + if (!vram) return; + u64 r[6] = { *(volatile u64 *)0x12000000, *(volatile u64 *)0x12000020, + *(volatile u64 *)0x12000070, *(volatile u64 *)0x12000080, *(volatile u64 *)0x12000090, *(volatile u64 *)0x120000A0 }; + if (!mtgs) { + u32 w, h; + gs_drain(); + rec_vsync(r); + if (gsgl_on) { gl_present(r); if (!plat_present(0, 0, 0)) { uya_log("window closed"); exit(0); } return; } + if (gs_capture(r[0], r[1], r[2], r[3], r[4], r[5], frame_px, &w, &h) && !plat_present(frame_px, (int)w, (int)h)) { uya_log("window closed"); exit(0); } + return; + } + gs_fifo_put(r, 3, 0xFFFFFFFEu); /* 48 bytes: 3 qwords */ + vs_sent++; + { /* no more than two vsyncs ahead of the GS */ + int spin = 0; + while (vs_sent - __atomic_load_n(&vs_done, __ATOMIC_ACQUIRE) > 2) cpu_relax(spin++); + } + static u32 shown; + u32 d = __atomic_load_n(&vs_done, __ATOMIC_ACQUIRE); + if (gsgl_on) { if (!plat_present(0, 0, 0)) { uya_log("window closed"); exit(0); } return; } /* the GS thread showed it */ + if (d != shown) { + u32 k = (d - 1) % 3; + shown = d; + if (cap_ok[k] && !plat_present(cap_buf[k], (int)cap_w[k], (int)cap_h[k])) { uya_log("window closed"); exit(0); } + } else if (!plat_present(0, 0, 0)) { uya_log("window closed"); exit(0); } /* nothing new: keep the window alive */ +} + +/* Writes a w x h image of RGBA8 pixels into GS memory as PSMCT32 at block + * address tbp, buffer width tbw (in 64-pixel units). The movie player uses it + * to put a decoded frame where the PS2's IPU output would be. */ +void uya_gs_write_ct32(u32 tbp, u32 tbw, int w, int h, const u32 *rgba) +{ + if (!vram) return; + gs_sync(); + gs_drain(); + if (rec_f && rec_vs > rec_from) { u32 hd[5] = { 3, tbp, tbw, (u32)w, (u32)h }; rec_put(hd, 20); rec_put(rgba, (size_t)w * h * 4); } + gl_mem_req(1, 0, tbp, tbw, 0, 0, (u32)w, (u32)h); + mark_blocks(tbp, area_blocks(0, tbw, (u32)h)); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) vram[a32(tbp, tbw, (u32)x, (u32)y)] = rgba[y * w + x]; +} + +/* GS -> memory (sceGsExecStoreImage): the w x h area at (x, y) of the buffer + * at block bp (width bw, format psm), packed row by row */ +void uya_gs_store_image(u32 bp, u32 bw, u32 psm, u32 x, u32 y, u32 w, u32 h, void *dst) +{ + if (!vram) return; + gs_sync(); + gs_drain(); + gl_mem_req(0, psm, bp, bw, x, y, w, h); + if (bw == 1 && psm == 0 && w == 64 && h == 64 && bp < 0x4000) si_hash[bp] = vhash(bp, 0x40) | 1; + u32 bits = trx_bits(psm), n = 0; + u8 *o = dst; + for (u32 j = 0; j < h; j++) + for (u32 i = 0; i < w; i++, n++) { + u32 v = px_read(psm, bp, bw, (x + i) & 2047, (y + j) & 2047); + switch (bits) { + case 4: if (n & 1) o[n >> 1] = (u8)((o[n >> 1] & 0x0F) | (v << 4)); else o[n >> 1] = (u8)((o[n >> 1] & 0xF0) | (v & 15)); break; + case 8: o[n] = (u8)v; break; + case 16: memcpy(o + n * 2, &v, 2); break; + case 24: memcpy(o + n * 3, &v, 3); break; + default: memcpy(o + n * 4, &v, 4); break; + } + } +} + +void uya_gs_stats(void) +{ + uya_log("GS: %llu primitives drawn, %llu pixels written", stat_prims, stat_pixels); +} + +/* GL: a level of a state's texture decoded now (for gs_gl.c's deferred levels) */ +int gs_tex_late(const pcmd_t *p, int lvl, tlevel_t *out) +{ + gs_ctx_t c; memset(&c, 0, sizeof c); + c.tex0 = p->tex0; c.tex1 = p->tex1; c.clamp = p->clamp; c.miptbp1 = p->miptbp1; c.miptbp2 = p->miptbp2; + u64 sv = texa; texa = p->texa; + prim_serial++; /* not the memo of the state being set up */ + const tcache_t *e = tc_get(&c, lvl); + prim_serial++; + texa = sv; + u32 b_, bw_, lw_, lh_; tex_level(&c, lvl, &b_, &bw_, &lw_, &lh_); + out->px = e->px; out->w = (int)e->w; out->h = (int)e->h; out->lw = (int)lw_; out->lh = (int)lh_; + out->tci = (int)(e - tc); out->ser = e->ser; + return 1; +} diff --git a/games/rac1/host/src/gs/gs_gl.c b/games/rac1/host/src/gs/gs_gl.c new file mode 100644 index 0000000..f7c4242 --- /dev/null +++ b/games/rac1/host/src/gs/gs_gl.c @@ -0,0 +1,1879 @@ +/* gs_gl.c: the GS's drawing on the GPU (OpenGL 3.3 core), PCSX2-hardware- + * renderer style but kept small. + * + * gs.c stays the front end: GIF, registers, GS local memory (vram), transfers, + * CLUTs and the texture cache (textures decoded to RGBA). Here: + * - targets: GL textures standing for GS buffers, by (base, width, format + * class): RGBA8 for colour (PSMCT32/24/16/16S), DEPTH32F for Z (Z32/24/16). + * Pixel (x, y) of the buffer is the sc x sc texels from (x*sc, y*sc) (no flip): + * sc is the upscale (UYA_SCALE, 1 = native). Transfers between GS memory and + * a target take one texel of each block (down) or fill the block (up). + * - coherence by 8 KB page: owner[p] is the target whose copy of page p is + * newer than vram (drawn on the GPU), and each target has valid[p] (its copy + * is current). A draw makes its pages valid (uploading from vram, after + * downloading other owners) and then owns them; CPU reads download owned + * pages; CPU writes download partly written ones and invalidate all copies. + * - primitives are batched per GS state (gs.c's state ring index) and drawn + * with one shader that does the GS pixel pipeline: texture fetch with the + * GS's wrap modes, nearest/bilinear and mip selection in integers (the + * same arithmetic as gs.c's tsample), TFX/TCC, fog, alpha test (AFAIL by a + * second pass), destination alpha test (a copy of the target), Z as + * integer values in a float depth buffer, and the blend (A-B)*C>>7+D with + * the source part computed in the shader and the destination part by + * fixed-function blending. + * - textures: decoded levels from gs.c's cache (RGBA8UI, texelFetch), or a + * colour target read directly (render to texture, the frame as a texture; + * copied first when it is also the draw target). Levels whose memory a + * target owns are decoded only when a batch's LOD range needs them (RAC1 + * points level 0 of distant mip chains at the frame buffer). + * - display: the target under DISPFB drawn into the window. */ +#include +#include +#include +#include +#include "uya_runtime.h" +#include "gs_types.h" +#include "gs_swizzle.h" +#include "gs_gl.h" +#include "gs_gl_api.h" + +/* ---- the GL thread ---- + * GL calls cost the driver CPU time (Kerwan: ~2600 draws a vsync). With the GL + * thread (default on; UYA_GLTHREAD=0 off) the GS thread only records them: each + * p_glX (gs_gl_thread.h, generated by tools/gen_glthread.py) appends the call + * and copies of the data it points at to a ring, which the GL thread, owner of + * the context, replays through the real entry points r_glX. Calls that return + * something run on the GL thread while the GS thread waits (rare: readbacks, + * gens at setup). The GS thread stays at most 2 presented frames ahead. */ +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif +typedef struct { volatile int sleeping; +#ifdef _WIN32 + HANDLE ev; +#else + sem_t sem; +#endif +} glt_park_t; +static void glt_park_init(glt_park_t *k) +{ + k->sleeping = 0; +#ifdef _WIN32 + k->ev = CreateEventA(0, FALSE, FALSE, 0); +#else + sem_init(&k->sem, 0, 0); +#endif +} +static void glt_park_wait(glt_park_t *k) /* until a wake-up, or 1 ms */ +{ + __atomic_store_n(&k->sleeping, 1, __ATOMIC_SEQ_CST); +#ifdef _WIN32 + WaitForSingleObject(k->ev, 1); +#else + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 1000000; if (ts.tv_nsec >= 1000000000) { ts.tv_nsec -= 1000000000; ts.tv_sec++; } + sem_timedwait(&k->sem, &ts); +#endif + __atomic_store_n(&k->sleeping, 0, __ATOMIC_RELEASE); +} +static void glt_park_wake(glt_park_t *k) +{ + if (!__atomic_load_n(&k->sleeping, __ATOMIC_SEQ_CST)) return; +#ifdef _WIN32 + SetEvent(k->ev); +#else + sem_post(&k->sem); +#endif +} +static inline void glt_relax(unsigned *spin) +{ + if (++*spin < 64) __builtin_ia32_pause(); +#ifdef _WIN32 + else if (*spin < 256) SwitchToThread(); +#else + else if (*spin < 256) sched_yield(); +#endif +} +#ifndef _WIN32 +#include +#endif + +#define GLT_WORDS ((size_t)8 << 20) /* 64 MB of u64 words */ +#define GLT_WRAP 0xFFFFu +#define GLT_SYNC 0x10000u +static int glt_q; /* recording (the GL thread runs) */ +static u64 *glt_ring; +static size_t glt_head, glt_tail, glt_pend; /* words: published, replayed (GL thread), written */ +static volatile u32 glt_swaps_sent, glt_swaps_done; +static glt_park_t glt_kpark, glt_ppark; /* the GL thread's and the GS thread's sleeps */ +static GLint glt_unpack_row, glt_unpack_align = 4; +static double glt_wait_ms; /* the GS thread's time waiting on the GL thread */ + +static inline u64 glt_fbits(float f) { u32 b; memcpy(&b, &f, 4); return b; } +static inline float glt_bitsf(u64 v) { u32 b = (u32)v; float f; memcpy(&f, &b, 4); return f; } +static void glt_track_store(GLenum p, GLint v) +{ + if (p == 0x0CF2 /* GL_UNPACK_ROW_LENGTH */) glt_unpack_row = v; + else if (p == 0x0CF5 /* GL_UNPACK_ALIGNMENT */) glt_unpack_align = v ? v : 1; +} +static size_t glt_pix_bytes(GLsizei w, GLsizei h, GLenum fmt, GLenum type) +{ + size_t comp = (fmt == 0x1903 || fmt == 0x8D94 || fmt == 0x1902) ? 1 : (fmt == 0x8227 || fmt == 0x8228) ? 2 : (fmt == 0x1907 || fmt == 0x80E0) ? 3 : 4; + size_t sz = (type == 0x1401 || type == 0x1400) ? 1 : (type == 0x1403 || type == 0x1402 || type == 0x140B) ? 2 : 4; + size_t bpp = comp * sz, rl = (size_t)(glt_unpack_row > 0 ? glt_unpack_row : w), al = (size_t)glt_unpack_align; + size_t stride = (rl * bpp + al - 1) / al * al; + return h > 0 && w > 0 ? stride * (size_t)(h - 1) + (size_t)w * bpp : 0; +} +static double glt_now_ms(void) +{ +#ifdef _WIN32 + static LARGE_INTEGER f; LARGE_INTEGER c; + if (!f.QuadPart) QueryPerformanceFrequency(&f); + QueryPerformanceCounter(&c); + return (double)c.QuadPart * 1000.0 / (double)f.QuadPart; +#else + struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; +#endif +} +static void glt_publish(void) +{ + if (glt_pend == glt_head) return; + __atomic_store_n(&glt_head, glt_pend, __ATOMIC_RELEASE); + glt_park_wake(&glt_kpark); +} +/* room for a call of nargs arguments and nb bytes of data; returns where its arguments go */ +static size_t glt_wr; /* end of the last complete call (published in chunks) */ +static u64 *glt_cmd_x(u32 id, int nargs, size_t nb) +{ + glt_wr = glt_pend; /* the call before this one is complete */ + if (glt_wr - glt_head >= 2048) { size_t p = glt_pend; glt_pend = glt_wr; glt_publish(); glt_pend = p; } /* 16 KB at a time */ + size_t words = 2 + (size_t)nargs + (nb + 7) / 8; + size_t pos = glt_pend % GLT_WORDS; + if (pos + words > GLT_WORDS) { /* not enough room before the end: start over at the beginning */ + glt_ring[pos] = GLT_WRAP; + glt_pend += GLT_WORDS - pos; pos = 0; + glt_wr = glt_pend; glt_publish(); + } + if (glt_pend + words - __atomic_load_n(&glt_tail, __ATOMIC_ACQUIRE) > GLT_WORDS) { /* full: wait for the GL thread */ + double t0 = glt_now_ms(); unsigned spin = 0; + while (glt_pend + words - __atomic_load_n(&glt_tail, __ATOMIC_ACQUIRE) > GLT_WORDS) { + if (spin > 256) glt_park_wait(&glt_ppark); else glt_relax(&spin); + } + glt_wait_ms += glt_now_ms() - t0; + } + u64 *c = glt_ring + pos; + c[0] = id; c[1] = nb; + glt_pend += words; + return c + 2; +} +static u64 *glt_cmd(u32 id, int nargs, size_t nb) { return glt_cmd_x(id, nargs, nb); } +/* everything recorded so far replayed */ +static void glt_sync(void) +{ + if (!glt_q) return; + glt_publish(); + double t0 = glt_now_ms(); unsigned spin = 0; + while (__atomic_load_n(&glt_tail, __ATOMIC_ACQUIRE) != glt_pend) { if (spin > 256) glt_park_wait(&glt_ppark); else glt_relax(&spin); } + glt_wait_ms += glt_now_ms() - t0; +} +/* a call with a result: run by the GL thread, waited for */ +static void glt_call(u32 id, const u64 *a, int nargs, u64 *r) +{ + u64 *c = glt_cmd_x(id | GLT_SYNC, nargs + 1, 0); + for (int i = 0; i < nargs; i++) c[i] = a[i]; + c[nargs] = (u64)(uintptr_t)r; + glt_sync(); +} +static void glt_exec_extra(u32 id, const u64 *c, size_t blob_n); +#include "gs_gl_thread.h" +static void glt_exec_extra(u32 id, const u64 *c, size_t blob_n) +{ + switch (id) { + case GLT_SWAP: plat_gl_swap(); __atomic_add_fetch(&glt_swaps_done, 1, __ATOMIC_RELEASE); glt_park_wake(&glt_ppark); break; + case GLT_VB: { /* vertices into the bound array buffer at an offset (c[0]), unsynchronized like the direct path */ + void *m = r_glMapBufferRange(0x8892 /* GL_ARRAY_BUFFER */, (GLintptr)c[0], (GLsizeiptr)blob_n, 0x0002 | 0x0020 /* WRITE | UNSYNCHRONIZED */); + if (m) { memcpy(m, c + 1, blob_n); r_glUnmapBuffer(0x8892); } + break; } + default: break; + } +} +/* a large texture upload through a pixel unpack buffer (orphaned each time): the driver + * copies it to the GPU when it can instead of before glTexSubImage2D returns */ +static GLuint glt_pbo; +static double glt_busy_ms; /* the GL thread's time running calls */ +static void glt_tex_pbo(const u64 *c, size_t nb) +{ + if (!glt_pbo) r_glGenBuffers(1, &glt_pbo); + r_glBindBuffer(0x88EC /* GL_PIXEL_UNPACK_BUFFER */, glt_pbo); + r_glBufferData(0x88EC, (GLsizeiptr)nb, 0, 0x88E0 /* GL_STREAM_DRAW */); + r_glBufferSubData(0x88EC, 0, (GLsizeiptr)nb, c + 9); + r_glTexSubImage2D((GLenum)c[0], (GLint)c[1], (GLint)c[2], (GLint)c[3], (GLsizei)c[4], (GLsizei)c[5], (GLenum)c[6], (GLenum)c[7], (const void *)0); + r_glBindBuffer(0x88EC, 0); +} +#ifdef _WIN32 +static DWORD WINAPI glt_main(LPVOID unused) +#else +static void *glt_main(void *unused) +#endif +{ + (void)unused; + plat_gl_current(1); + unsigned spin = 0; + for (;;) { + size_t h = __atomic_load_n(&glt_head, __ATOMIC_ACQUIRE), t = glt_tail; + if (t == h) { if (spin > 256) glt_park_wait(&glt_kpark); else glt_relax(&spin); continue; } + spin = 0; + while (t != h) { + const u64 *c = glt_ring + t % GLT_WORDS; + u32 hd = (u32)c[0]; + if (hd == GLT_WRAP) { t += GLT_WORDS - t % GLT_WORDS; continue; } + u32 id = hd & 0xFFFF; + size_t nb = (size_t)c[1]; + /* arguments: count from the generated table is not stored; sync calls keep their result pointer last */ + int na = glt_nargs[id] + ((hd & GLT_SYNC) ? 1 : 0); + double e0 = glt_now_ms(); + if (id == GLT_glTexSubImage2D && nb >= 16384) glt_tex_pbo(c + 2, nb); + else glt_exec(id, c + 2, nb, (hd & GLT_SYNC) ? (u64 *)(uintptr_t)c[2 + na - 1] : 0); + glt_busy_ms += glt_now_ms() - e0; + t += 2 + (size_t)na + (nb + 7) / 8; + if (hd & GLT_SYNC) { __atomic_store_n(&glt_tail, t, __ATOMIC_RELEASE); glt_park_wake(&glt_ppark); } + } + __atomic_store_n(&glt_tail, t, __ATOMIC_RELEASE); + glt_park_wake(&glt_ppark); + } +#ifdef _WIN32 + return 0; +#else + return 0; +#endif +} +static void glt_start(void) +{ + if (glt_q) return; /* (initialised again: the thread runs already) */ + const char *e = getenv("UYA_GLTHREAD"); + if (e && !atoi(e)) return; + glt_ring = (u64 *)malloc(GLT_WORDS * 8); + if (!glt_ring) return; + glt_park_init(&glt_kpark); glt_park_init(&glt_ppark); + plat_gl_current(0); /* the context goes to the GL thread */ +#ifdef _WIN32 + CreateThread(0, 0, glt_main, 0, 0, 0); +#else + pthread_t th; pthread_create(&th, 0, glt_main, 0); pthread_detach(th); +#endif + glt_q = 1; + uya_log("GL GS: GL calls on their own thread"); +} +int gsgl_threaded(void) { return glt_q; } +/* GL thread time since the last call: running GL calls, and the GS thread waiting for it (ms) */ +void gsgl_thread_times(double *busy, double *wait) { *busy = glt_busy_ms; *wait = glt_wait_ms; glt_busy_ms = 0; glt_wait_ms = 0; } +/* present: the swap is recorded too; the GS thread stays at most 2 frames ahead */ +static void glt_swap(void) +{ + if (!glt_q) { plat_gl_swap(); return; } + glt_cmd(GLT_SWAP, 0, 0); + glt_publish(); + glt_swaps_sent++; + double t0 = glt_now_ms(); unsigned spin = 0; + while (glt_swaps_sent - __atomic_load_n(&glt_swaps_done, __ATOMIC_ACQUIRE) > 2) { if (spin > 256) glt_park_wait(&glt_ppark); else glt_relax(&spin); } + glt_wait_ms += glt_now_ms() - t0; +} + +#define BITS(v, lo, n) ((u32)(((v) >> (lo)) & ((1ull << (n)) - 1))) + +int gsgl_on; +static u32 *vram; +static int sc = 1; /* the upscale: each GS pixel is sc x sc texels of a target */ + +/* ---- addressing (as gs.c) ---- */ +static inline u32 A32(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 5) * bw + (x >> 6)) * 32 + gs_block32[(y >> 3) & 3][(x >> 3) & 7]; + return ((b & 0x3FFF) << 6) | gs_col32[y & 7][x & 7]; +} +static inline u32 A32Z(u32 bp, u32 bw, u32 x, u32 y) +{ + u32 b = bp + ((y >> 5) * bw + (x >> 6)) * 32 + (gs_block32[(y >> 3) & 3][(x >> 3) & 7] ^ 0x18); + return ((b & 0x3FFF) << 6) | gs_col32[y & 7][x & 7]; +} +static inline u32 A16(u32 bp, u32 bw, u32 x, u32 y, const uint8_t (*bt)[4], u32 z) +{ + u32 b = bp + ((y >> 6) * bw + (x >> 6)) * 32 + (bt[(y >> 3) & 7][(x >> 4) & 3] ^ z); + return ((b & 0x3FFF) << 7) | gs_col16[y & 7][x & 15]; +} + +/* ---- targets ---- */ +enum { TC32, TC16, TC16S, TZ32, TZ24, TZ16, TZ16S }; +static int cls_of(u32 psm) +{ + switch (psm) { + case 0x00: case 0x01: return TC32; + case 0x02: return TC16; + case 0x0A: return TC16S; + case 0x30: return TZ32; + case 0x31: return TZ24; + case 0x32: return TZ16; + case 0x3A: return TZ16S; + } + return -1; +} +#define CLS_Z(c) ((c) >= TZ32) +#define CLS_16(c) ((c) == TC16 || (c) == TC16S || (c) == TZ16 || (c) == TZ16S) + +#define NTG 24 +#define TGH 1024 /* rows of a target texture */ +typedef struct { + int used; u32 bp, bw, cls; int w, ph, rows; GLuint tex; u32 lru; + u8 valid[512]; /* this copy of the page is current */ +} tgt_t; +static tgt_t tg[NTG]; +static signed char owner[512]; /* the target whose copy is newer than vram, or -1 */ +static u32 lru_clock; + +static struct { unsigned long long flushes, draws, prims, dl_pages, dl_reads, ul_pages, tex_ul, copies, late, atlas_resets; } st, st0; + +/* GL objects */ +static GLuint prog, vao, vbo, prog_p, vao_p; +static GLint u_i, u_apass, u_vp, up_src, up_lim, up_tsz, up_mode, up_ps; +static GLuint dummy_ui, dummy_f; +static GLuint rd_fbo, rd_fbo_z, tmp_tex[2], tmp_fbo[2], cap_tex, cap_fbo; +static GLuint tcgl[TC_N]; static u32 tcser[TC_N]; static int tcw[TC_N], tch[TC_N]; +/* Decoded texture levels live in one big integer texture (an atlas), so a + * batch can use many textures: each primitive carries a texture record (the + * levels' places in the atlas and the wrap and filter settings), and + * primitives that differ only in their texture are drawn together (RAC1's + * menu changes texture almost every primitive: 2000+ draws a frame before). + * tcax/tcay: an entry's place (tcgl[i] = 1: placed); shelves of power-of-two + * heights; when the atlas is full it starts over (the batch is drawn first). */ +#define ATLAS 4096 +static GLuint atlas_tex; static int tcax[TC_N], tcay[TC_N]; static u32 atlas_gen, tcagen[TC_N]; +static struct { int y, h, x; } shelf[1024]; static int nshelf, shelf_top; +#define REC_SZ 18 /* ivec4s per texture record */ +#define REC_MAX 512 +static GLint recs[REC_MAX][REC_SZ * 4]; static int nrec; static GLuint rec_buf, rec_tex; +static struct { GLuint c, z, fbo; } fbc[48]; static int nfbc; + +/* GL state the batches set (cached; ~0 = unknown) */ +static struct { GLuint fbo, prog; int blend, depth; GLenum eq, sf, df, dfunc; float bcol; int dmask, cmask; GLuint tex[10]; int vw, vh, sc[4]; } gc; +static void gc_reset(void) { memset(&gc, 0xFF, sizeof gc); } + +static void logonce(int *f, const char *m) { if (!*f) { *f = 1; uya_log("GL GS: %s", m); } } + +static GLuint mk_tex(GLenum ifmt, int w, int h, GLenum fmt, GLenum type, int linear) +{ + GLuint t; p_glGenTextures(1, &t); + p_glActiveTexture(GL_TEXTURE0 + 15); + p_glBindTexture(GL_TEXTURE_2D, t); + p_glTexImage2D(GL_TEXTURE_2D, 0, (GLint)ifmt, w, h, 0, fmt, type, 0); + p_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + p_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + p_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + p_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + p_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + return t; +} +static void bind_unit(int u, GLuint t) +{ + if (u < 9 && gc.tex[u] == t) return; + p_glActiveTexture(GL_TEXTURE0 + u); + p_glBindTexture(GL_TEXTURE_2D, t); + if (u < 9) gc.tex[u] = t; +} + +static inline u32 tpage(const tgt_t *t, int col, int row) { return (t->bp / 32 + (u32)row * t->bw + (u32)col) & 511; } + +static void download(tgt_t *t, const u16 *pg, int n); +static void fbo_forget(GLuint tex); +static tgt_t *tgt_get(u32 bp, u32 bw, int cls, const tgt_t *keep1, const tgt_t *keep2) +{ + int fr = -1, lru = -1; + for (int i = 0; i < NTG; i++) { + tgt_t *t = &tg[i]; + if (!t->used) { if (fr < 0) fr = i; continue; } + if (t->bp == bp && t->bw == bw && t->cls == (u32)cls) { t->lru = ++lru_clock; return t; } + if (t != keep1 && t != keep2 && (lru < 0 || t->lru < tg[lru].lru)) lru = i; + } + if (fr < 0) { /* evict the least recently used: its newer pages go to vram first */ + tgt_t *t = &tg[lru]; + u16 pg[512]; int n = 0; + for (int p = 0; p < 512; p++) if (owner[p] == lru) pg[n++] = (u16)p; + if (n) download(t, pg, n); + fbo_forget(t->tex); + p_glDeleteTextures(1, &t->tex); + for (int u = 0; u < 9; u++) if (gc.tex[u] == t->tex) gc.tex[u] = ~0u; + t->used = 0; + fr = lru; + } + tgt_t *t = &tg[fr]; + memset(t, 0, sizeof *t); + t->used = 1; t->bp = bp; t->bw = bw; t->cls = (u32)cls; t->w = (int)bw * 64; t->lru = ++lru_clock; + t->ph = CLS_16(cls) ? 64 : 32; + t->rows = TGH / t->ph; + if ((u32)t->rows * bw > 512) t->rows = (int)(512 / bw); + if (CLS_Z(cls)) t->tex = mk_tex(GL_DEPTH_COMPONENT32F, t->w * sc, TGH * sc, GL_DEPTH_COMPONENT, GL_FLOAT, 0); + else t->tex = mk_tex(GL_RGBA8, t->w * sc, TGH * sc, GL_RGBA, GL_UNSIGNED_BYTE, 1); + return t; +} + +static GLuint fbo_get(GLuint c, GLuint z) +{ + for (int i = 0; i < nfbc; i++) if (fbc[i].c == c && fbc[i].z == z) return fbc[i].fbo; + if (nfbc == 48) { /* full: start over (deleted, or the textures they hold would never be freed) */ + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); gc.fbo = ~0u; + for (int i = 0; i < nfbc; i++) p_glDeleteFramebuffers(1, &fbc[i].fbo); + nfbc = 0; + } + GLuint f; p_glGenFramebuffers(1, &f); + p_glBindFramebuffer(GL_FRAMEBUFFER, f); + p_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, c, 0); + p_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, z, 0); + gc.fbo = f; + fbc[nfbc].c = c; fbc[nfbc].z = z; fbc[nfbc].fbo = f; nfbc++; + return f; +} +static void fbo_forget(GLuint tex) +{ + for (int i = 0; i < nfbc; i++) + if (fbc[i].c == tex || fbc[i].z == tex) { + /* deleted with the texture: an FBO that still holds a deleted texture keeps its memory + * alive, so every evicted target leaked its texture (GPU memory grew over a session) */ + if (gc.fbo == fbc[i].fbo) { p_glBindFramebuffer(GL_FRAMEBUFFER, 0); gc.fbo = ~0u; } + p_glDeleteFramebuffers(1, &fbc[i].fbo); + fbc[i] = fbc[--nfbc]; i--; + } +} +static void bind_fbo(GLuint f) { if (gc.fbo != f) { p_glBindFramebuffer(GL_FRAMEBUFFER, f); gc.fbo = f; } } + +/* ---- pages between vram and targets ---- */ +static u32 *xbuf; static size_t xbuf_n; +static void *xb(size_t n) { if (n > xbuf_n) { free(xbuf); xbuf = malloc(n); xbuf_n = n; } return xbuf; } + +static const char *dl_why = "?"; +/* the target's newer copy of these pages into vram */ +static void download(tgt_t *t, const u16 *pg, int n) +{ + int ti = (int)(t - tg); + int c0 = 1 << 30, c1 = -1, r0 = 1 << 30, r1 = -1; + for (int i = 0; i < n; i++) { + u32 k = (pg[i] - t->bp / 32) & 511; + int row = (int)(k / t->bw), col = (int)(k % t->bw); + if (col < c0) c0 = col; + if (col > c1) c1 = col; + if (row < r0) r0 = row; + if (row > r1) r1 = row; + } + if (c1 < 0) return; + { /* testing: UYA_GLDLLOG=1 logs every readback and why */ + static int dbg = -1; if (dbg < 0) dbg = getenv("UYA_GLDLLOG") != 0; + if (dbg) uya_log("download v%u: target %04X/%u cls %u, %d pages from %u (%s)", uya_frame_count(), t->bp, t->bw, t->cls, n, pg[0], dl_why); } + int x0 = c0 * 64, y0 = r0 * t->ph, w = (c1 - c0 + 1) * 64, h = (r1 - r0 + 1) * t->ph; + void *buf = xb((size_t)w * h * 4 * sc * sc); + int z = CLS_Z(t->cls); + p_glBindFramebuffer(GL_READ_FRAMEBUFFER, z ? rd_fbo_z : rd_fbo); + p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, z ? GL_DEPTH_ATTACHMENT : GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, t->tex, 0); + p_glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glReadPixels(x0 * sc, y0 * sc, w * sc, h * sc, z ? GL_DEPTH_COMPONENT : GL_RGBA, z ? GL_FLOAT : GL_UNSIGNED_BYTE, buf); + p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, z ? GL_DEPTH_ATTACHMENT : GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + p_glBindFramebuffer(GL_READ_FRAMEBUFFER, gc.fbo == ~0u ? 0 : gc.fbo); + st.dl_reads++; + u16 *v16 = (u16 *)vram; + for (int i = 0; i < n; i++) { + u32 k = (pg[i] - t->bp / 32) & 511; + int row = (int)(k / t->bw), col = (int)(k % t->bw); + if (owner[pg[i]] == ti) owner[pg[i]] = -1; + st.dl_pages++; + for (int yy = 0; yy < t->ph; yy++) { + u32 y = (u32)(row * t->ph + yy); + /* (upscaled: the texel at the middle of each pixel's block) */ + const u32 *src = (const u32 *)buf + ((size_t)(y - (u32)y0) * sc + (size_t)(sc / 2)) * (size_t)(w * sc) + (size_t)(col * 64 - x0) * sc + (size_t)(sc / 2); + const float *fs0 = (const float *)src; + for (u32 xx = 0; xx < 64; xx++) { + u32 x = (u32)col * 64 + xx, v = src[xx * sc], a; + float fz = fs0[xx * sc]; + switch (t->cls) { + case TC32: vram[A32(t->bp, t->bw, x, y)] = v; break; + case TC16: case TC16S: + v16[A16(t->bp, t->bw, x, y, t->cls == TC16 ? gs_block16 : gs_block16s, 0)] = + (u16)(((v >> 3) & 31) | (((v >> 11) & 31) << 5) | (((v >> 19) & 31) << 10) | ((v >> 31) << 15)); + break; + case TZ32: { double d = (double)fz * 4294967296.0; vram[A32Z(t->bp, t->bw, x, y)] = d >= 4294967295.0 ? 0xFFFFFFFFu : (u32)(d + 0.5); } break; + case TZ24: a = A32Z(t->bp, t->bw, x, y); vram[a] = (vram[a] & 0xFF000000u) | ((u32)(fz * 16777216.0f + 0.5f) & 0xFFFFFF); break; + default: { u32 zz = (u32)(fz * 65536.0f + 0.5f); v16[A16(t->bp, t->bw, x, y, t->cls == TZ16 ? gs_block16 : gs_block16s, 0x18)] = (u16)(zz > 0xFFFF ? 0xFFFF : zz); } break; + } + } + } + } +} +/* vram into the target's copy of a page */ +static void upload_page(tgt_t *t, int col, int row) +{ + u32 *buf = xb((size_t)64 * 64 * 4 * (sc * sc + 1)); + u16 *v16 = (u16 *)vram; + for (int yy = 0; yy < t->ph; yy++) { + u32 y = (u32)(row * t->ph + yy); + u32 *o = buf + yy * 64; + float *of = (float *)o; + for (u32 xx = 0; xx < 64; xx++) { + u32 x = (u32)col * 64 + xx, v; + switch (t->cls) { + case TC32: o[xx] = vram[A32(t->bp, t->bw, x, y)]; break; + case TC16: case TC16S: + v = v16[A16(t->bp, t->bw, x, y, t->cls == TC16 ? gs_block16 : gs_block16s, 0)]; + o[xx] = ((v & 31) << 3) | (((v >> 5) & 31) << 11) | (((v >> 10) & 31) << 19) | ((v & 0x8000) ? 0x80000000u : 0); + break; + case TZ32: of[xx] = (float)((double)vram[A32Z(t->bp, t->bw, x, y)] * (1.0 / 4294967296.0)); break; + case TZ24: of[xx] = (float)(vram[A32Z(t->bp, t->bw, x, y)] & 0xFFFFFF) * (1.0f / 16777216.0f); break; + default: of[xx] = (float)v16[A16(t->bp, t->bw, x, y, t->cls == TZ16 ? gs_block16 : gs_block16s, 0x18)] * (1.0f / 65536.0f); break; + } + } + } + if (sc > 1) { /* upscaled: each pixel fills its sc x sc block */ + u32 *big = buf + 64 * 64, W = 64 * (u32)sc; + for (int yy = 0; yy < t->ph; yy++) + for (int k = 0; k < sc; k++) { + u32 *o = big + ((size_t)yy * sc + k) * W; + for (u32 xx = 0; xx < 64; xx++) for (int j = 0; j < sc; j++) o[xx * sc + j] = buf[yy * 64 + xx]; + } + buf = big; + } + p_glActiveTexture(GL_TEXTURE0 + 15); + p_glBindTexture(GL_TEXTURE_2D, t->tex); + p_glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + p_glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + if (CLS_Z(t->cls)) p_glTexSubImage2D(GL_TEXTURE_2D, 0, col * 64 * sc, row * t->ph * sc, 64 * sc, t->ph * sc, GL_DEPTH_COMPONENT, GL_FLOAT, buf); + else p_glTexSubImage2D(GL_TEXTURE_2D, 0, col * 64 * sc, row * t->ph * sc, 64 * sc, t->ph * sc, GL_RGBA, GL_UNSIGNED_BYTE, buf); + st.ul_pages++; +} + +/* downloads pages (any owner) to vram, one read per owner */ +static void download_any(const u16 *pg, int n, const u8 *skip) +{ + static u16 lst[NTG][512]; int cnt[NTG] = {0}; + for (int i = 0; i < n; i++) { + int o = owner[pg[i]]; + if (o < 0 || (skip && skip[i])) continue; + lst[o][cnt[o]++] = pg[i]; + } + for (int o = 0; o < NTG; o++) if (cnt[o]) download(&tg[o], lst[o], cnt[o]); +} + +/* UYA_GL_STRICT=1: also when a texture reads memory another target owns past + * what it draws (RAC1's frame copy reads a row of the Z buffer at + * 6%, distant mip chains point level 0 at the display buffer), sync it through + * GS memory like the software GS sees it; by default those reads see the last + * synced contents, which saves a GPU readback (a pipeline stall) or two a frame */ +static int strict = -1, no_dl; +static u8 *cover; /* pages the batch overwrites whole (no upload needed), or 0 */ +/* the target's copy of the pages under a rectangle (pixels, inclusive) made current */ +static void ensure_valid(tgt_t *t, int x0, int y0, int x1, int y1) +{ + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > t->w - 1) x1 = t->w - 1; + if (y1 > t->rows * t->ph - 1) y1 = t->rows * t->ph - 1; + if (x0 > x1 || y0 > y1) return; + int ti = (int)(t - tg); + u16 pg[512]; int n = 0; + for (int r = y0 / t->ph; r <= y1 / t->ph; r++) + for (int c = x0 / 64; c <= x1 / 64; c++) { + u32 p = tpage(t, c, r); + if (no_dl && owner[p] >= 0 && owner[p] != ti) continue; /* (UYA_GL_STRICT) */ + if (cover && cover[p] && !t->valid[p]) { t->valid[p] = 1; continue; } /* overwritten whole: its old contents do not matter */ + if (!t->valid[p]) { pg[n++] = (u16)p; } + } + if (!n) return; + { /* other targets' newer copies to vram first */ + u16 o[512]; int m = 0; + for (int i = 0; i < n; i++) if (owner[pg[i]] >= 0 && owner[pg[i]] != ti) o[m++] = pg[i]; + if (m) { static char why[64]; snprintf(why, sizeof why, "pages for target %04X/%u cls %u", t->bp, t->bw, t->cls); dl_why = why; download_any(o, m, 0); } + } + for (int i = 0; i < n; i++) { + u32 k = (pg[i] - t->bp / 32) & 511; + upload_page(t, (int)(k % t->bw), (int)(k / t->bw)); + t->valid[pg[i]] = 1; + } +} +/* a draw wrote the pages under a rectangle */ +static void claim(tgt_t *t, int x0, int y0, int x1, int y1) +{ + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > t->w - 1) x1 = t->w - 1; + if (y1 > t->rows * t->ph - 1) y1 = t->rows * t->ph - 1; + int ti = (int)(t - tg); + for (int r = y0 / t->ph; r <= y1 / t->ph; r++) + for (int c = x0 / 64; c <= x1 / 64; c++) { + u32 p = tpage(t, c, r); + if (owner[p] == ti) continue; + owner[p] = (signed char)ti; + for (int i = 0; i < NTG; i++) if (i != ti && tg[i].used) tg[i].valid[p] = 0; + } +} + +/* the vram pages under a rectangle of a buffer; full[i]: the rectangle covers the whole page */ +static int rect_pages(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h, u16 *out, u8 *full) +{ + u32 pw = 64, ph = 32, ppr = bw ? bw : 1; + switch (psm) { + case 0x02: case 0x0A: case 0x32: case 0x3A: ph = 64; break; + case 0x13: pw = 128; ph = 64; ppr = bw > 1 ? bw / 2 : 1; break; + case 0x14: pw = 128; ph = 128; ppr = bw > 1 ? bw / 2 : 1; break; + } + if (!w || !h) return 0; + if (x > 2047) x = 2047; + if (y > 2047) y = 2047; + if (x + w > 2048) w = 2048 - x; + if (y + h > 2048) h = 2048 - y; + static u8 seen[512]; static u32 gen_[512], gen; + gen++; + int n = 0; + for (u32 py = y / ph; py <= (y + h - 1) / ph; py++) + for (u32 px = x / pw; px <= (x + w - 1) / pw; px++) { + u32 b0 = bp + (py * ppr + px) * 32; + int whole = !(b0 & 31) && x <= px * pw && x + w >= (px + 1) * pw && y <= py * ph && y + h >= (py + 1) * ph; + u32 span = 1; + if (b0 & 31) { /* a base inside a page: the last block the rectangle reaches decides whether it goes on into the next page */ + u32 cx0 = x > px * pw ? x - px * pw : 0, cx1 = x + w < (px + 1) * pw ? x + w - px * pw - 1 : pw - 1; + u32 cy0 = y > py * ph ? y - py * ph : 0, cy1 = y + h < (py + 1) * ph ? y + h - py * ph - 1 : ph - 1, mb = 0; + int tall = (pw == 64 && ph == 64) || (pw == 128 && ph == 128); /* 16 and 4-bit: 4 x 8 blocks, else 8 x 4 */ + u32 bwid = tall ? pw / 4 : pw / 8, bhgt = tall ? ph / 8 : ph / 4; + for (u32 yy = cy0 / bhgt; yy <= cy1 / bhgt; yy++) + for (u32 xx = cx0 / bwid; xx <= cx1 / bwid; xx++) { + u32 bi = (psm & 0x30) == 0x30 ? 31 : (tall ? (psm == 0x0A ? gs_block16s[yy & 7][xx & 3] : gs_block16[yy & 7][xx & 3]) : gs_block32[yy & 3][xx & 7]); + if (bi > mb) mb = bi; + } + span = ((b0 & 31) + mb) >= 32 ? 2 : 1; + } + for (u32 k = 0; k < span; k++) { + u32 p = (b0 / 32 + k) & 511; + if (gen_[p] == gen) { if (!whole) for (int i = 0; i < n; i++) if (out[i] == p) full[i] = 0; continue; } + gen_[p] = gen; seen[p] = 1; + out[n] = (u16)p; full[n] = (u8)whole; n++; + } + } + return n; +} + +/* ---- textures from the cache ---- */ +static int b_on; +static int gc_apass = -1; +static int getenv_cached_nomerge(void) { static int v = -1; if (v < 0) v = getenv("UYA_GL_NOMERGE") != 0; return v; } +static u32 b_clut[256], b_cluth; static int b_clut_saved; /* the palette a batch's deferred levels decode with */ +static u32 rec_clut[512][256], rec_cluth[512]; /* each record's palette (for its levels decoded late) */ +void gs_clut_swap(u32 *pal, u32 *hash); /* gs.c: exchange clut[] and its hash with these */ +static void clut_snap(int r) { u32 t[256], h = 0; gs_clut_swap(t, &h); memcpy(rec_clut[r], t, sizeof t); rec_cluth[r] = h; gs_clut_swap(t, &h); } +static pcmd_t bs; +static int in_flush; +void gsgl_flush(void); +static int rec_tci[REC_MAX][7], rec_mxl[REC_MAX]; +/* texture cache slots the open batch refers to (gs.c does not reuse them until it is drawn: + * a slot taken by another texture made the batch decode the old one again at flush time, + * with whatever palette was loaded by then: textures in the wrong colours for a frame) */ +u32 gsgl_tc_pin[TC_N], gsgl_batch_id = 1; +static void pin_levels(const pcmd_t *S) +{ + if (!S->tme || S->rt) return; + for (int l = 0; l <= S->mxl && l < 7; l++) { int i = S->lv[l].tci; if (i >= 0 && i < TC_N) gsgl_tc_pin[i] = gsgl_batch_id; } +} +static int batch_uses(int tci) +{ + if (!b_on || !bs.tme || bs.rt) return 0; + for (int l = 0; l <= bs.mxl && l < 7; l++) if (!(bs.defer & (1 << l)) && bs.lv[l].tci == tci) return 1; + for (int r = 0; r < nrec; r++) for (int l = 0; l <= rec_mxl[r] && l < 7; l++) if (rec_tci[r][l] == tci) return 1; + return 0; +} +static int atlas_place(int w, int h, int *ax, int *ay) +{ + static int lim = -1; /* testing: UYA_ATLAS_LIMIT=n uses only n x n of the atlas (it fills and starts over often) */ + if (lim < 0) { const char *e = getenv("UYA_ATLAS_LIMIT"); lim = e ? atoi(e) : ATLAS; if (lim < 256 || lim > ATLAS) lim = ATLAS; } + int sh = 8; while (sh < h) sh <<= 1; + if (w > lim || sh > lim) return 0; + for (int i = 0; i < nshelf; i++) + if (shelf[i].h == sh && shelf[i].x + w <= lim) { *ax = shelf[i].x; *ay = shelf[i].y; shelf[i].x += (w + 3) & ~3; return 1; } + if (nshelf == 1024 || shelf_top + sh > lim) return 0; + shelf[nshelf].y = shelf_top; shelf[nshelf].h = sh; shelf[nshelf].x = (w + 3) & ~3; nshelf++; + *ax = 0; *ay = shelf_top; shelf_top += sh; + return 1; +} +void gsgl_flush(void); +void gsgl_tex_touch(const tlevel_t *l) +{ + int i = l->tci; + if (i < 0 || i >= TC_N || !l->px) return; + if (tcser[i] == l->ser && tcgl[i] && tcagen[i] == atlas_gen) return; + if (!in_flush && batch_uses(i)) gsgl_flush(); + int ax, ay; + if (tcgl[i] && tcagen[i] == atlas_gen && tcw[i] == l->w && tch[i] == l->h) { ax = tcax[i]; ay = tcay[i]; } + else if (!atlas_place(l->w, l->h, &ax, &ay)) { + /* full: start over (what the batch being drawn uses must stay: drawn first) */ + if (!in_flush && b_on) gsgl_flush(); + atlas_gen++; nshelf = 0; shelf_top = 0; st.atlas_resets++; + if (!atlas_place(l->w, l->h, &ax, &ay)) return; + } + p_glActiveTexture(GL_TEXTURE0 + 15); + p_glBindTexture(GL_TEXTURE_2D, atlas_tex); + p_glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + p_glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + p_glTexSubImage2D(GL_TEXTURE_2D, 0, ax, ay, l->w, l->h, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, l->px); + tcgl[i] = 1; tcax[i] = ax; tcay[i] = ay; tcw[i] = l->w; tch[i] = l->h; tcagen[i] = atlas_gen; + tcser[i] = l->ser; + st.tex_ul++; +} +static inline int tc_ready(const tlevel_t *l) +{ + int ci = l->tci; + return l->px && ci >= 0 && ci < TC_N && tcgl[ci] && tcser[ci] == l->ser && tcagen[ci] == atlas_gen; +} + +/* the vram pages a texture level covers (exactly, unlike gs.c's slack) */ +static int tex_pages(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th, u16 *out, u8 *full) +{ + u32 bw = tbw ? tbw : 1; + switch (psm) { /* the H formats and 24-bit read the 32-bit layout */ + case 0x1B: case 0x24: case 0x2C: case 0x01: psm = 0; break; + } + if (tw > bw * 64 && psm != 0x13 && psm != 0x14) tw = bw * 64; + return rect_pages(psm, tbp, bw, 0, 0, tw, th, out, full); +} +int gsgl_tex_gpu(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th) +{ + { /* quick: no target owns anything in the whole span of pages the buffer can reach */ + u32 bits = (psm == 0x14 || psm == 0x24 || psm == 0x2C) ? 4 : (psm == 0x13 || psm == 0x1B) ? 8 : (psm == 0x02 || psm == 0x0A) ? 16 : 32; + u32 w = (tbw ? tbw : 1) * 64, np = (w * th * bits / 8 + 8191) / 8192 + 1, p0 = tbp / 32, any = 0; + if (bits == 4 || bits == 8) np += (w * 2 * th * bits / 8 + 8191) / 8192; /* (their pages are 128 wide) */ + if (np > 512) np = 512; + for (u32 i = 0; i <= np && !any; i++) any = owner[(p0 + i) & 511] >= 0; + if (!any) return 0; + } + u16 pg[512]; u8 full[512]; + int n = tex_pages(psm, tbp, tbw, tw, th, pg, full); + for (int i = 0; i < n; i++) if (owner[pg[i]] >= 0) return 1; + return 0; +} + +/* a texture that is a colour target: read on the GPU */ +int gsgl_tex_setup(pcmd_t *p) +{ + u32 tbp = BITS(p->tex0, 0, 14), tbw = BITS(p->tex0, 14, 6), psm = BITS(p->tex0, 20, 6); + if ((psm != 0x00 && psm != 0x01) || (tbp & 31) || !tbw) return 0; + if (p->mmin >= 2 && BITS(p->tex1, 2, 3)) { /* mipmapped: from a target only if the LOD is constant and picks level 0 */ + float k = floorf(p->k * 16.0f + 0.5f) / 16.0f; + if (!(p->fst || p->lcm) || k > ((p->mmin == 3 || p->mmin == 5) ? 0.0f : 0.49f)) return 0; + } + int ti = -1; + for (int i = 0; i < NTG; i++) if (tg[i].used && tg[i].bp == tbp && tg[i].bw == tbw && tg[i].cls == TC32) { ti = i; break; } + if (ti < 0) return 0; + u32 tw = 1u << BITS(p->tex0, 26, 4), th = 1u << BITS(p->tex0, 30, 4); + u16 pg[512]; u8 full[512]; + int n = tex_pages(0, tbp, tbw, tw, th, pg, full), mine = 0; + for (int i = 0; i < n; i++) { + if (owner[pg[i]] == ti) mine = 1; + } + if (!mine) return 0; + p->rt = (u16)(ti + 1); + return 1; +} + +void gsgl_clut_changed(void) {} /* (each record keeps the palette it was drawn with) */ + +/* ---- the CPU side's reads and writes ---- */ +void gsgl_cpu_read_rect(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h) +{ + if (!in_flush) gsgl_flush(); + u16 pg[512]; u8 full[512]; + int n = rect_pages(psm, bp, bw, x, y, w, h, pg, full); + dl_why = "CPU read of a rectangle"; download_any(pg, n, 0); +} +/* 1 if the batch being gathered may draw on page p (its frame or Z buffer) */ +static int batch_page(u32 p) +{ + if (!b_on) return 0; + u32 rows = (u32)(bs.sy1 < 0 ? 0 : bs.sy1) + 1, w = bs.fbw ? bs.fbw : 1; + u32 f0 = bs.fbp / 32, fn = ((rows + 31) / 32) * w + 1; /* (16-bit pages hold 64 rows: this is more than enough) */ + if (((p - f0) & 511) < fn) return 1; + if (bs.zte || !bs.zmsk) { u32 z0 = bs.zbp / 32; if (((p - z0) & 511) < fn) return 1; } + return 0; +} +void gsgl_cpu_read(u32 bp, u32 nblocks) +{ + u16 pg[512]; int n = 0, any = 0; + if (!nblocks) nblocks = 1; + u32 p0 = bp / 32, p1 = (bp + nblocks - 1) / 32; + if (p1 - p0 >= 512) p1 = p0 + 511; + for (u32 p = p0; p <= p1; p++) { pg[n++] = (u16)(p & 511); if (owner[p & 511] >= 0 || batch_page(p & 511)) any = 1; } + /* memory no GPU target has and the batch does not draw on: read as it is + * (the CLUT loads of RAC1's menu: ending the batch for each cost a draw) */ + if (!any) return; + if (!in_flush) gsgl_flush(); + { static char w[64]; snprintf(w, sizeof w, "CPU read from %p", __builtin_return_address(0)); dl_why = w; } + download_any(pg, n, 0); +} +void gsgl_tex_read(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th) +{ + u16 pg[512]; u8 full[512]; + int n = tex_pages(psm, tbp, tbw, tw, th, pg, full), any = 0; + u8 skip[512]; + for (int i = 0; i < n; i++) { + int o = owner[pg[i]]; + /* (not UYA_GL_STRICT) a level read in another format than a target drew there: garbage the game never shows */ + skip[i] = o >= 0 && no_dl && (int)tg[o].cls != cls_of(psm); + if (o >= 0 && !skip[i]) any = 1; + } + if (!any) return; + if (!in_flush) gsgl_flush(); + dl_why = "texture decode"; download_any(pg, n, skip); +} +/* would gsgl_tex_read download pages of this level (a GPU target owns some, and they are + * not skipped): then GS memory there is not what the level's decode will read */ +int gsgl_tex_owned(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th) +{ + u16 pg[512]; u8 full[512]; + int n = tex_pages(psm, tbp, tbw, tw, th, pg, full); + for (int i = 0; i < n; i++) { + int o = owner[pg[i]]; + if (o >= 0 && !(no_dl && (int)tg[o].cls != cls_of(psm))) return 1; + } + return 0; +} +void gsgl_cpu_write_rect(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h) +{ + if (!in_flush) gsgl_flush(); + u16 pg[512]; u8 full[512]; + int n = rect_pages(psm, bp, bw, x, y, w, h, pg, full); + static char why[96]; snprintf(why, sizeof why, "CPU write psm %X bp %04X bw %u %u,%u %ux%u", psm, bp, bw, x, y, w, h); dl_why = why; + download_any(pg, n, full); /* pages the write only partly covers keep the rest */ + for (int i = 0; i < n; i++) { + owner[pg[i]] = -1; + for (int k = 0; k < NTG; k++) tg[k].valid[pg[i]] = 0; + } +} + +/* ---- batches ---- */ +/* a vertex: Z as a base (the primitive's smallest Z, flat) plus a float offset, + * so Z interpolates to the unit even near 2^24 and 2^32 */ +typedef struct { float x, y, U, V, Q, dz; u32 zr; u8 r, g, b, a; u8 f, div, p0, p1; } gv_t; +static u32 zref; +static int cur_rec; /* the texture record of the primitives being added */ +static int seg_v[4096], nseg; /* where each run of one record's primitives starts (vertex index) */ +static float seg_bb[4096][4]; /* each run's bounding box */ +static pcmd_t rec_st[REC_MAX]; /* the states of records 1.. (record 0 is the batch's own) */ +static gv_t *vb; static int nv, vcap; +static int b_mode; static u32 b_st; +static float bx0, by0, bx1, by1, bqmin, bqmax, bu0, bu1, bv0, bv1; +static int b_uvall; /* the texture coordinates can be anywhere (perspective through Q = 0) */ +/* a batch of sprites only (clears, copies): their pixel rectangles, while they do not overlap */ +#define NSPR 64 +static int b_spr, b_nspr; static int b_sr[NSPR][4]; + +static inline void put(float x, float y, float U, float V, float Q, u32 z, const vtx_t *c, u8 f, u8 div) +{ + gv_t *o = &vb[nv++]; + if (bs.zpsm == 0x31) z &= 0xFFFFFF; /* Z24 uses the low 24 bits */ + o->x = x; o->y = y; o->U = U; o->V = V; o->Q = Q; o->zr = zref; o->dz = (float)(z - zref); + o->r = c->r; o->g = c->g; o->b = c->b; o->a = c->a; o->f = f; o->div = div; o->p0 = (u8)cur_rec; o->p1 = (u8)(cur_rec >> 8); + if (x < bx0) bx0 = x; + if (x > bx1) bx1 = x; + if (y < by0) by0 = y; + if (y > by1) by1 = y; + float *sb = seg_bb[nseg - 1]; + if (x < sb[0]) sb[0] = x; + if (x > sb[1]) sb[1] = x; + if (y < sb[2]) sb[2] = y; + if (y > sb[3]) sb[3] = y; +} +static inline void uvb(float u, float v) +{ + if (u < bu0) bu0 = u; + if (u > bu1) bu1 = u; + if (v < bv0) bv0 = v; + if (v > bv1) bv1 = v; +} +static inline void qb(float q) +{ + if (q < bqmin) bqmin = q; + if (q > bqmax) bqmax = q; +} + +/* S differs from the batch's state only in its texture (and neither reads a + * GPU target nor has levels decoded late) */ +static int tex_only_change(const pcmd_t *S) +{ + if (!bs.tme || !S->tme || bs.rt || S->rt || nrec >= REC_MAX - 1 || b_mode != GL_TRIANGLES) { + if (getenv("UYA_GLBRK")) { static unsigned long long c[8], tot; c[!bs.tme ? 0 : !S->tme ? 1 : bs.rt ? 2 : S->rt ? 3 : bs.defer ? 4 : S->defer ? 5 : 6]++; + if (++tot % 20000 == 0) uya_log("merge refused: tme %llu/%llu rt %llu/%llu defer %llu/%llu other %llu", c[0], c[1], c[2], c[3], c[4], c[5], c[6]); } + return 0; + } + if (getenv_cached_nomerge()) return 0; + if (getenv("UYA_GL_NOMERGEDEF") && (S->defer || bs.defer)) return 0; + pcmd_t t = *S; + memcpy(t.lv, bs.lv, sizeof t.lv); + t.tex0 = bs.tex0; t.tex1 = bs.tex1; t.miptbp1 = bs.miptbp1; t.miptbp2 = bs.miptbp2; t.clamp = bs.clamp; t.texa = bs.texa; + t.tw = bs.tw; t.th = bs.th; t.k = bs.k; t.lcm = bs.lcm; t.mxl = bs.mxl; t.mmag = bs.mmag; t.mmin = bs.mmin; t.l = bs.l; + t.wms = bs.wms; t.wmt = bs.wmt; t.minu = bs.minu; t.maxu = bs.maxu; t.minv = bs.minv; t.maxv = bs.maxv; t.tfx = bs.tfx; t.tcc = bs.tcc; t.defer = bs.defer; + if (memcmp(&t, &bs, sizeof t)) { + if (getenv("UYA_GLBRK")) { /* testing: the first differing byte offset */ + static unsigned long long cnt[1024], tot; const u8 *a = (const u8 *)&t, *b = (const u8 *)&bs; size_t o = 0; + while (o < sizeof t && a[o] == b[o]) o++; + if (o < 1024) cnt[o]++; + if (++tot % 20000 == 0) for (int i = 0; i < 1024; i++) if (cnt[i] > tot / 50) uya_log("merge refused at offset %d: %llu", i, cnt[i]); + } + return 0; + } + return 1; +} +void gsgl_prim(const pcmd_t *S, u32 sti, const pprim_t *pr) +{ + int mode = (pr->type == 3 || pr->type == 6) ? GL_TRIANGLES : GL_POINTS; + if (b_on && sti != b_st && getenv("UYA_GLBRK")) { /* testing: what changes between batches */ + static unsigned long long n[16], tot; const char *nm[16] = {"tex","blend","test","frame","scissor","wrap","fog","prim","mode","other"}; + int k = 9; + if (memcmp(&bs.lv, &S->lv, sizeof bs.lv) || bs.tex0 != S->tex0) k = 0; + else if (bs.abe != S->abe || bs.ba != S->ba || bs.bb != S->bb || bs.bc != S->bc || bs.bd != S->bd || bs.fix != S->fix) k = 1; + else if (bs.ate != S->ate || bs.atst != S->atst || bs.aref != S->aref || bs.afail != S->afail || bs.zte != S->zte || bs.ztst != S->ztst || bs.zmsk != S->zmsk || bs.date != S->date) k = 2; + else if (bs.fbp != S->fbp || bs.fbw != S->fbw || bs.zbp != S->zbp || bs.fbmsk != S->fbmsk) k = 3; + else if (bs.sx0 != S->sx0 || bs.sx1 != S->sx1 || bs.sy0 != S->sy0 || bs.sy1 != S->sy1) k = 4; + else if (bs.wms != S->wms || bs.wmt != S->wmt || bs.minu != S->minu || bs.maxu != S->maxu || bs.minv != S->minv || bs.maxv != S->maxv || bs.clamp != S->clamp) k = 5; + else if (bs.fge != S->fge || bs.fogr != S->fogr) k = 6; + else if (bs.tme != S->tme || bs.fst != S->fst || bs.iip != S->iip || bs.tfx != S->tfx || bs.tcc != S->tcc) k = 7; + else if (bs.tex1 != S->tex1 || bs.mmag != S->mmag || bs.mmin != S->mmin || bs.texa != S->texa) k = 8; + n[k]++; if (++tot % 40000 == 0) { char b[400]; int o = 0; for (int i = 0; i < 10; i++) o += snprintf(b + o, sizeof b - o, "%s %llu ", nm[i], n[i]); uya_log("batch breaks: %s", b); } + } + if (b_on && sti != b_st && mode == b_mode && nv + 6 <= vcap && tex_only_change(S)) { + /* the same but for the texture: a new texture record in the same batch */ + if (cur_rec == 0 || memcmp(&rec_st[cur_rec], S, sizeof *S)) { + if (nrec == 0) nrec = 1; /* (record 0: the batch's own state) */ + rec_st[nrec] = *S; + rec_mxl[nrec] = S->mxl; + if (S->defer) clut_snap(nrec); + for (int l = 0; l < 7; l++) rec_tci[nrec][l] = S->lv[l].tci; + pin_levels(S); + cur_rec = nrec++; + } + if (nseg < 4096) { seg_v[nseg] = nv; seg_bb[nseg][0] = seg_bb[nseg][2] = 1e30f; seg_bb[nseg][1] = seg_bb[nseg][3] = -1e30f; nseg++; } + else { gsgl_flush(); } + b_st = sti; + } + if (b_on && (sti != b_st || mode != b_mode || nv + 6 > vcap)) gsgl_flush(); + if (!b_on) { + nrec = 0; cur_rec = 0; nseg = 1; seg_v[0] = 0; seg_bb[0][0] = seg_bb[0][2] = 1e30f; seg_bb[0][1] = seg_bb[0][3] = -1e30f; + if (S->ztst == 0 || !S->fbw) return; /* nothing passes the depth test */ + if (S->ate && S->atst == 0 && S->afail == 0) return; /* nor the alpha test */ + if (!vb) { vcap = 1 << 16; vb = malloc(sizeof(gv_t) * (size_t)vcap); } + bs = *S; b_st = sti; b_mode = mode; b_on = 1; nv = 0; b_uvall = 0; b_spr = 1; b_nspr = 0; b_clut_saved = 0; + pin_levels(S); + if (S->tme && S->defer && !S->rt) { clut_snap(0); b_clut_saved = 1; } + bx0 = by0 = bu0 = bv0 = bqmin = 1e30f; bx1 = by1 = bu1 = bv1 = bqmax = -1e30f; + } + st.prims++; + const vtx_t *v = pr->v; + const int tme = bs.tme, fst = bs.fst; + switch (pr->type) { + case 3: { + b_spr = 0; + u32 m = bs.zpsm == 0x31 ? 0xFFFFFF : 0xFFFFFFFFu, z0 = v[0].z & m, z1 = v[1].z & m, z2 = v[2].z & m; + zref = z0 < z1 ? z0 : z1; if (z2 < zref) zref = z2; + for (int k = 0; k < 3; k++) { + if (!tme) put(v[k].x, v[k].y, 0, 0, 1, v[k].z, &v[k], v[k].f, 0); + else if (fst) { put(v[k].x, v[k].y, v[k].u, v[k].v, 1, v[k].z, &v[k], v[k].f, 0); uvb(v[k].u, v[k].v); } + else { + float U = v[k].s * S->tw, V = v[k].t * S->th, Q = v[k].q; + put(v[k].x, v[k].y, U, V, Q, v[k].z, &v[k], v[k].f, 1); + qb(Q); + if (Q > 0) uvb(U / Q, V / Q); else b_uvall = 1; + } + } + if (tme && !fst && ((bqmin <= 0) != (bqmax <= 0))) b_uvall = 1; + } break; + case 6: { + const vtx_t *a = &v[0], *b = &v[1]; + if (b_spr) { + int r[4] = { (int)ceilf(fminf(a->x, b->x)), (int)ceilf(fmaxf(a->x, b->x)) - 1, (int)ceilf(fminf(a->y, b->y)), (int)ceilf(fmaxf(a->y, b->y)) - 1 }; + if (b_nspr == NSPR) b_spr = 0; + for (int i = 0; i < b_nspr && b_spr; i++) + if (r[0] <= b_sr[i][1] && b_sr[i][0] <= r[1] && r[2] <= b_sr[i][3] && b_sr[i][2] <= r[3]) b_spr = 0; + if (b_spr && r[1] >= r[0] && r[3] >= r[2]) { memcpy(b_sr[b_nspr], r, sizeof r); b_nspr++; } + } + float u0 = 0, v0 = 0, u1 = 0, v1 = 0, q = 1; + if (tme) { + if (fst) { u0 = a->u; v0 = a->v; u1 = b->u; v1 = b->v; } + else { + q = b->q; + u0 = a->s * S->tw / q; v0 = a->t * S->th / q; u1 = b->s * S->tw / q; v1 = b->t * S->th / q; + qb(q); + } + { /* the texels the pixels' sample points reach (x from ceil(x0) to ceil(x1) - 1) */ + float xa = fminf(a->x, b->x), xb = fmaxf(a->x, b->x), ya = fminf(a->y, b->y), yb = fmaxf(a->y, b->y); + float px0 = ceilf(xa), px1 = ceilf(xb) - 1, py0 = ceilf(ya), py1 = ceilf(yb) - 1; + if (px1 >= px0 && py1 >= py0 && xb > xa && yb > ya) { + float ua = a->x <= b->x ? u0 : u1, ub = a->x <= b->x ? u1 : u0, va = a->y <= b->y ? v0 : v1, vb2 = a->y <= b->y ? v1 : v0; + float du = (ub - ua) / (xb - xa), dv = (vb2 - va) / (yb - ya); + uvb(ua + (px0 - xa) * du, va + (py0 - ya) * dv); uvb(ua + (px1 - xa) * du, va + (py1 - ya) * dv); + } else { uvb(u0, v0); uvb(u1, v1); } + } + } + float x0 = a->x, y0 = a->y, x1 = b->x, y1 = b->y; + zref = bs.zpsm == 0x31 ? (b->z & 0xFFFFFF) : b->z; + put(x0, y0, u0, v0, q, b->z, b, b->f, 0); put(x1, y0, u1, v0, q, b->z, b, b->f, 0); put(x0, y1, u0, v1, q, b->z, b, b->f, 0); + put(x1, y0, u1, v0, q, b->z, b, b->f, 0); put(x1, y1, u1, v1, q, b->z, b, b->f, 0); put(x0, y1, u0, v1, q, b->z, b, b->f, 0); + } break; + case 1: { /* lines: the pixels gs.c's r_line picks (a DDA rounding to the nearest pixel), as points */ + const vtx_t *a = &v[0], *b = &v[1]; + float ua = 0, va = 0, ub = 0, vb_ = 0, q = 1; + if (tme) { + if (fst) { ua = a->u; va = a->v; ub = b->u; vb_ = b->v; } + else { ua = a->s * bs.tw / a->q; va = a->t * bs.th / a->q; ub = b->s * bs.tw / b->q; vb_ = b->t * bs.th / b->q; q = b->q; qb(q); } + uvb(ua, va); uvb(ub, vb_); + } + float dx = b->x - a->x, dy = b->y - a->y; + int n = (int)ceilf(fmaxf(fabsf(dx), fabsf(dy))); + if (n <= 0) n = 1; + if (n > 4096) n = 4096; + zref = bs.zpsm == 0x31 ? (b->z & 0xFFFFFF) : b->z; + for (int i = 0; i < n; i++) { + if (nv + 1 > vcap) { gsgl_flush(); bs = *S; b_st = sti; b_mode = mode; b_on = 1; nv = 0; pin_levels(S); } + float t = (float)i / n; + vtx_t c = *a; + c.r = (u8)(a->r + (b->r - a->r) * t); c.g = (u8)(a->g + (b->g - a->g) * t); c.b = (u8)(a->b + (b->b - a->b) * t); c.a = (u8)(a->a + (b->a - a->a) * t); + put(floorf(a->x + dx * t + 0.5f), floorf(a->y + dy * t + 0.5f), ua + (ub - ua) * t, va + (vb_ - va) * t, q, b->z, &c, b->f, 0); + } + } break; + default: { + float u = 0, w = 0, q = 1; + if (tme) { + if (fst) { u = v[0].u; w = v[0].v; } + else { q = v[0].q; u = v[0].s * bs.tw; w = v[0].t * bs.th; if (q != 0) { u /= q; w /= q; } qb(q); } + uvb(u, w); + } + zref = bs.zpsm == 0x31 ? (v[0].z & 0xFFFFFF) : v[0].z; + put(floorf(v[0].x), floorf(v[0].y), u, w, q, v[0].z, &v[0], v[0].f, 0); + } break; + } +} + +/* the mip levels the batch's LOD range can use */ +static void lod_levels(const pcmd_t *p, int *lmin, int *lmax) +{ + int mxl = p->mxl; + *lmin = 0; *lmax = 0; + if (p->mmin < 2 || mxl == 0) return; + float lo, hi; + if (p->lcm || p->fst || bqmax < bqmin) lo = hi = p->k; + else { + float a = fabsf(bqmin), b = fabsf(bqmax); + float qa = (bqmin <= 0 && bqmax >= 0) ? 0 : (a < b ? a : b), qb_ = a > b ? a : b; + float s = (float)(1 << p->l); + lo = qb_ > 0 ? -log2f(qb_) * s + p->k : 1e9f; + hi = qa > 0 ? -log2f(qa) * s + p->k : 1e9f; + } + lo -= 0.07f; hi += 0.07f; /* the LOD's rounding to 1/16 and gs.c's log2 approximation */ + if (lo <= 0) *lmin = 0; + else *lmin = (p->mmin == 3 || p->mmin == 5) ? (int)floorf(lo) : (int)floorf(lo + 0.5f); + if (hi <= 0) *lmax = 0; + else *lmax = (p->mmin == 3 || p->mmin == 5) ? (int)floorf(hi) + 1 : (int)floorf(hi + 0.5f); + if (*lmin > mxl) *lmin = mxl; + if (*lmax > mxl) *lmax = mxl; + if (*lmin < 0) *lmin = 0; +} + +static int lg_partial, lg_alias, lg_ad, lg_blend, lg_fmt, lg_clamp; +/* the blend: the source part S (shader, integers) and the destination's + * factor (fixed function). (A - B) * C >> 7 + D = Cs * (a C' + d) + Cd * (a' C' + d'). */ +static void blend_setup(const pcmd_t *p, int *bla, int *bld, int *neg, GLenum *eq, GLenum *df, float *bc) +{ + int A = p->ba, B = p->bb, D = p->bd; + int a = (A == 0) - (B == 0), d = D == 0, a2 = (A == 1) - (B == 1), d2 = D == 1; + if (A == B) { a = 0; a2 = 0; } + if (p->bc == 1) logonce(&lg_ad, "blend C = destination alpha (taken as 1.0)"); + GLenum C = p->bc == 0 ? GL_SRC1_ALPHA : GL_CONSTANT_ALPHA, Cm = p->bc == 0 ? GL_ONE_MINUS_SRC1_ALPHA : GL_ONE_MINUS_CONSTANT_ALPHA; + if (p->bc == 1) { C = GL_ONE; Cm = GL_ZERO; } + *bc = p->bc == 2 ? (p->fix > 128 ? 1.0f : p->fix / 128.0f) : 1.0f; + int dsign; + if (a2 == 0 && d2 == 0) { *df = GL_ZERO; dsign = 1; } + else if (a2 == 0) { *df = GL_ONE; dsign = 1; } + else if (a2 == 1 && d2 == 0) { *df = C; dsign = 1; } + else if (a2 == -1 && d2 == 0) { *df = C; dsign = -1; } + else if (a2 == -1 && d2 == 1) { *df = Cm; dsign = 1; } + else { *df = GL_ONE; dsign = 1; logonce(&lg_blend, "blend Cd * (1 + C) (taken as Cd)"); } + int ssign = (a == -1 && d == 0) ? -1 : 1; + *bla = a; *bld = d; *neg = ssign < 0; + if (ssign > 0 && dsign > 0) *eq = GL_FUNC_ADD; + else if (ssign > 0) *eq = GL_FUNC_SUBTRACT; + else if (dsign > 0) *eq = GL_FUNC_REVERSE_SUBTRACT; + else { *eq = GL_FUNC_ADD; *df = GL_ZERO; *bla = 0; *bld = 0; *neg = 0; } /* <= 0: black */ +} + +static void copy_rect(GLuint src_tex, int which, int x0, int y0, int x1, int y1) +{ + if (!tmp_tex[which]) { + tmp_tex[which] = mk_tex(GL_RGBA8, 2048 * sc, TGH * sc, GL_RGBA, GL_UNSIGNED_BYTE, 0); + p_glGenFramebuffers(1, &tmp_fbo[which]); + p_glBindFramebuffer(GL_FRAMEBUFFER, tmp_fbo[which]); + p_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tmp_tex[which], 0); + gc.fbo = ~0u; + for (int u = 0; u < 9; u++) if (gc.tex[u] == tmp_tex[which]) gc.tex[u] = ~0u; + } + p_glBindFramebuffer(GL_READ_FRAMEBUFFER, rd_fbo); + p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, src_tex, 0); + p_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, tmp_fbo[which]); + p_glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(x0 * sc, y0 * sc, (x1 + 1) * sc, (y1 + 1) * sc, x0 * sc, y0 * sc, (x1 + 1) * sc, (y1 + 1) * sc, GL_COLOR_BUFFER_BIT, GL_NEAREST); + gc.sc[0] = -1; /* the next batch enables and sets the scissor again */ + p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + gc.fbo = ~0u; + st.copies++; +} + +void gsgl_flush(void) +{ + if (!b_on) return; + if (getenv("UYA_GLBRK")) { /* testing: who ends batches */ + static void *ra[64]; static unsigned long long rn[64], tot; void *r = __builtin_return_address(0); int i; + for (i = 0; i < 64 && ra[i] && ra[i] != r; i++) ; + if (i < 64) { ra[i] = r; rn[i]++; } + if (++tot % 40000 == 0) for (i = 0; i < 64 && ra[i]; i++) uya_log("flush from %p: %llu", ra[i], rn[i]); + } + b_on = 0; + if (!nv || in_flush) { nv = 0; return; } + in_flush = 1; + st.flushes++; + if (strict < 0) { const char *e = getenv("UYA_GL_STRICT"); strict = e && atoi(e); } + pcmd_t *p = &bs; + int fc = cls_of(p->fpsm); + if (fc < 0 || CLS_Z(fc)) { logonce(&lg_fmt, "a frame buffer format it cannot draw"); goto done; } + tgt_t *ct = tgt_get(p->fbp, p->fbw, fc, 0, 0); + int x0 = (int)floorf(bx0) - 1, y0 = (int)floorf(by0) - 1, x1 = (int)ceilf(bx1) + 1, y1 = (int)ceilf(by1) + 1; + if (x0 < p->sx0) x0 = p->sx0; + if (y0 < p->sy0) y0 = p->sy0; + if (x1 > p->sx1) x1 = p->sx1; + if (y1 > p->sy1) y1 = p->sy1; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > ct->w - 1) x1 = ct->w - 1; + if (y1 > ct->rows * ct->ph - 1) y1 = ct->rows * ct->ph - 1; + if (x0 > x1 || y0 > y1) goto done; + tgt_t *zt = 0; + int zwrite = p->zte && !p->zmsk; + if (p->zte && (p->ztst != 1 || zwrite)) { + int zc = cls_of(p->zpsm); + if (zc >= 0) zt = tgt_get(p->zbp, p->fbw, zc, ct, 0); + if (zt) { /* Z in the frame's own pages (a clear of both): colour only */ + static u8 mark[512]; static u32 gen_[512], gen; gen++; + int hit = 0; + for (int r = y0 / ct->ph; r <= y1 / ct->ph; r++) for (int c = x0 / 64; c <= x1 / 64; c++) gen_[tpage(ct, c, r)] = gen; + for (int r = y0 / zt->ph; r <= y1 / zt->ph && !hit; r++) for (int c = x0 / 64; c <= x1 / 64; c++) if (gen_[tpage(zt, c, r)] == gen) { hit = 1; break; } + (void)mark; + if (hit) { + static int n; if (n++ < 5) uya_log("GL GS: Z buffer %04X over the frame buffer %04X (rows %d-%d): Z ignored", zt->bp, ct->bp, y0, y1); + zt = 0; + } + } + } + /* the target's current contents (before anything reads them); not for + * pages non-overlapping sprites overwrite whole whatever was there */ + static u8 cov_c[512], cov_z[512]; + int opaque = b_spr && b_nspr && b_mode == GL_TRIANGLES && (!p->ate || p->atst == 1) && !p->date; + int cc = opaque && !p->abe && !p->fbmsk && (fc == TC32 ? p->fpsm == 0x00 : 1) && !(p->tme && p->rt && &tg[p->rt - 1] == ct); + int cz = opaque && zt && zwrite && p->ztst == 1; + if (cc || cz) { + memset(cov_c, 0, sizeof cov_c); memset(cov_z, 0, sizeof cov_z); + for (int k = 0; k < 2; k++) { + tgt_t *t = k ? zt : ct; u8 *cv = k ? cov_z : cov_c; + if (!(k ? cz : cc)) continue; + for (int r = y0 / t->ph; r <= y1 / t->ph; r++) + for (int c = x0 / 64; c <= x1 / 64; c++) { + int px0 = c * 64, px1 = px0 + 63, py0 = r * t->ph, py1 = py0 + t->ph - 1, area = 0; + if (px0 < p->sx0 || px1 > p->sx1 || py0 < p->sy0 || py1 > p->sy1) continue; /* (the scissor cuts it) */ + for (int i = 0; i < b_nspr; i++) { + int w = (b_sr[i][1] < px1 ? b_sr[i][1] : px1) - (b_sr[i][0] > px0 ? b_sr[i][0] : px0) + 1; + int h = (b_sr[i][3] < py1 ? b_sr[i][3] : py1) - (b_sr[i][2] > py0 ? b_sr[i][2] : py0) + 1; + if (w > 0 && h > 0) area += w * h; + } + if (area == 64 * t->ph) cv[tpage(t, c, r)] = 1; + } + } + } + cover = cc ? cov_c : 0; + ensure_valid(ct, x0, y0, x1, y1); + cover = cz ? cov_z : 0; + if (zt) ensure_valid(zt, x0, y0, x1, y1); + cover = 0; + /* the texture */ + tgt_t *rt = 0; GLuint rtex = dummy_f; + tlevel_t lv[7]; int lmin = 0, lmax = 0; + memcpy(lv, p->lv, sizeof lv); + if (p->tme && p->rt) { + rt = &tg[p->rt - 1]; + if (!rt->used || rt->bp != BITS(p->tex0, 0, 14) || rt->bw != BITS(p->tex0, 14, 6) || rt->cls != TC32) { /* gone: decode it */ + rt = 0; p->rt = 0; p->defer = 1; + } + } + if (p->tme && rt) { + int tw = (int)p->tw, th = (int)p->th, u0 = 0, u1 = tw - 1, v0 = 0, v1 = th - 1; + if (!b_uvall && bu1 >= bu0) { + int lin = p->mmag || p->mmin == 1 || p->mmin == 4 || p->mmin == 5; /* bilinear reads x - 0.5 and the next */ + float h = lin ? 0.5f : 0.0f; + int a = (int)floorf(bu0 - h), b = (int)floorf(bu1 - h) + lin, c = (int)floorf(bv0 - h), d = (int)floorf(bv1 - h) + lin; + if (p->wms == 1) { a = a < 0 ? 0 : a; b = b > tw - 1 ? tw - 1 : b; } + else if (p->wms == 2) { a = a < p->minu ? p->minu : a; b = b > p->maxu ? p->maxu : b; } + if (p->wmt == 1) { c = c < 0 ? 0 : c; d = d > th - 1 ? th - 1 : d; } + else if (p->wmt == 2) { c = c < p->minv ? p->minv : c; d = d > p->maxv ? p->maxv : d; } + if ((p->wms == 0 && a >= 0 && b < tw) || p->wms == 1 || p->wms == 2) { u0 = a; u1 = b; } + if ((p->wmt == 0 && c >= 0 && d < th) || p->wmt == 1 || p->wmt == 2) { v0 = c; v1 = d; } + } + no_dl = !strict; + ensure_valid(rt, u0, v0, u1, v1); + no_dl = 0; + lv[0].lw = tw; lv[0].lh = th; lv[0].w = rt->w; lv[0].h = rt->rows * rt->ph; + if (rt == ct) { copy_rect(ct->tex, 0, u0 < 0 ? 0 : u0, v0 < 0 ? 0 : v0, u1 >= ct->w ? ct->w - 1 : u1, v1 >= TGH ? TGH - 1 : v1); rtex = tmp_tex[0]; } + else rtex = rt->tex; + } else if (p->tme) { + lod_levels(p, &lmin, &lmax); + if (b_clut_saved) gs_clut_swap(rec_clut[0], &rec_cluth[0]); /* the batch's palette */ + for (int l = 0; l <= p->mxl && l < 7; l++) { + int need = l >= lmin && l <= lmax; + if ((p->defer & (1 << l)) || tcser[lv[l].tci] != lv[l].ser) { + if (!need) continue; + no_dl = !strict; + if (gs_tex_late(p, l, &lv[l])) { gsgl_tex_touch(&lv[l]); st.late++; } + no_dl = 0; + } + } + if (b_clut_saved) gs_clut_swap(rec_clut[0], &rec_cluth[0]); /* back */ + } + /* the destination alpha test reads the frame as it was before the batch */ + int date = p->date && ct->cls == TC32 && p->fpsm == 0x00; + if (p->date && (ct->cls == TC16 || ct->cls == TC16S)) date = 1; + if (date) copy_rect(ct->tex, 1, x0, y0, x1, y1); + claim(ct, x0, y0, x1, y1); + if (zt && zwrite) claim(zt, x0, y0, x1, y1); + + /* GL state */ + bind_fbo(fbo_get(ct->tex, zt ? zt->tex : 0)); + if (gc.vw != ct->w * sc || gc.vh != TGH * sc) { p_glViewport(0, 0, ct->w * sc, TGH * sc); gc.vw = ct->w * sc; gc.vh = TGH * sc; } + if (gc.sc[0] != x0 || gc.sc[1] != y0 || gc.sc[2] != x1 || gc.sc[3] != y1) { + p_glEnable(GL_SCISSOR_TEST); + p_glScissor(x0 * sc, y0 * sc, (x1 - x0 + 1) * sc, (y1 - y0 + 1) * sc); + gc.sc[0] = x0; gc.sc[1] = y0; gc.sc[2] = x1; gc.sc[3] = y1; + } + if (gc.prog != prog) { p_glUseProgram(prog); p_glBindVertexArray(vao); gc.prog = prog; } + /* texture records: 0 is the batch's own state, 1.. the other textures merged in */ + if (nrec == 0) nrec = 1; + u32 gen0 = atlas_gen; + int place_tries = 0; + records_again: + for (int r = 0; r < nrec; r++) { + const pcmd_t *q = r ? &rec_st[r] : p; + tlevel_t rl[7]; + if (r) { + memcpy(rl, q->lv, sizeof rl); + int qmin = 0, qmax = 7; + if (q->defer) { lod_levels(q, &qmin, &qmax); gs_clut_swap(rec_clut[r], &rec_cluth[r]); } + for (int l = 0; l <= q->mxl && l < 7; l++) { + if (q->defer & (1 << l)) { /* decoded late: only the levels the batch's LOD range needs */ + if (l < qmin || l > qmax) continue; + no_dl = !strict; + if (gs_tex_late(q, l, &rl[l])) { gsgl_tex_touch(&rl[l]); st.late++; } + no_dl = 0; + } else if (!tc_ready(&rl[l])) { if (rl[l].px && tcser[rl[l].tci] != rl[l].ser) gs_tex_late(q, l, &rl[l]); gsgl_tex_touch(&rl[l]); } + } + if (q->defer) gs_clut_swap(rec_clut[r], &rec_cluth[r]); + memcpy(rec_st[r].lv, rl, sizeof rl); /* (decoded now: another pass of the loop finds them) */ + rec_st[r].defer = 0; + } else { + memcpy(rl, lv, sizeof rl); + for (int l = 0; l <= q->mxl && l < 7; l++) if (q->tme && !rt && rl[l].px && !tc_ready(&rl[l])) gsgl_tex_touch(&rl[l]); + } + GLint *R = recs[r]; + memset(R, 0, sizeof recs[r]); + R[0] = q->wms; R[1] = q->wmt; R[2] = q->minu; R[3] = q->maxu; + R[4] = q->minv; R[5] = q->maxv; R[6] = q->mmag; R[7] = q->mmin; + R[8] = q->mxl; R[9] = q->lcm; R[10] = q->l; { float k = q->k; memcpy(&R[11], &k, 4); } + R[12] = q->tfx; R[13] = q->tcc; + for (int l = 0; l < 7; l++) { + GLint *L4 = R + 16 + l * 8; + int ok = q->tme && (r || !rt) && l <= q->mxl && tc_ready(&rl[l]); + int ci = rl[l].tci; + L4[0] = ok ? tcax[ci] : 0; L4[1] = ok ? tcay[ci] : 0; + L4[2] = rl[l].w > 0 ? rl[l].w : 1; L4[3] = rl[l].h > 0 ? rl[l].h : 1; + if (!ok) { L4[2] = 1; L4[3] = 1; L4[0] = 0; L4[1] = 0; } + if (r == 0 && rt && l == 0) { L4[2] = lv[0].w; L4[3] = lv[0].h; } + L4[4] = rl[l].lw > 0 ? rl[l].lw : 1; L4[5] = rl[l].lh > 0 ? rl[l].lh : 1; + } + } + /* the atlas started over meanwhile: places again (at most twice: a batch whose textures + * do not fit in the atlas at all would otherwise start it over forever) */ + if (atlas_gen != gen0 && ++place_tries < 3) { gen0 = atlas_gen; goto records_again; } + p_glBindBuffer(GL_TEXTURE_BUFFER, rec_buf); + p_glBufferSubData(GL_TEXTURE_BUFFER, 0, (GLsizeiptr)(sizeof recs[0]) * nrec, recs); + if (gc.tex[9] != rec_tex) { p_glActiveTexture(GL_TEXTURE0 + 9); p_glBindTexture(GL_TEXTURE_BUFFER, rec_tex); gc.tex[9] = rec_tex; } + bind_unit(0, atlas_tex); + bind_unit(7, rtex); + bind_unit(8, date ? tmp_tex[1] : dummy_f); + int bla = 0, bld = 1, neg = 0; GLenum eq = GL_FUNC_ADD, df = GL_ZERO; float bcol = 1; + if (p->abe) blend_setup(p, &bla, &bld, &neg, &eq, &df, &bcol); + if (!p->clampc) logonce(&lg_clamp, "COLCLAMP off (wrapping) not done"); + { + int on = p->abe != 0; + if (gc.blend != on) { if (on) p_glEnable(GL_BLEND); else p_glDisable(GL_BLEND); gc.blend = on; } + if (on && (gc.eq != eq || gc.df != df)) { + p_glBlendEquationSeparate(eq, GL_FUNC_ADD); + p_glBlendFuncSeparate(GL_ONE, df, GL_ONE, GL_ZERO); + gc.eq = eq; gc.df = df; + } + if (on && gc.bcol != bcol) { p_glBlendColor(0, 0, 0, bcol); gc.bcol = bcol; } + } + { + int on = zt != 0; + if (gc.depth != on) { if (on) p_glEnable(GL_DEPTH_TEST); else p_glDisable(GL_DEPTH_TEST); gc.depth = on; } + GLenum f = p->ztst == 3 ? GL_GREATER : (p->ztst == 2 ? GL_GEQUAL : GL_ALWAYS); + if (on && gc.dfunc != f) { p_glDepthFunc(f); gc.dfunc = f; } + } + /* colour write mask (FBMSK whole bytes; PSMCT24 keeps alpha) */ + int cm = 15; + if (fc == TC32) { + for (int k = 0; k < 4; k++) { + u32 m = (p->fbmsk >> (k * 8)) & 255; + if (m == 255) cm &= ~(1 << k); + else if (m) logonce(&lg_partial, "FBMSK with partial bytes (taken as written)"); + } + if (p->fpsm == 0x01) cm &= 7; + } else if (p->fbmsk & 0xFFFF) { + if ((p->fbmsk & 0x8000) == 0x8000) cm &= 7; + if ((p->fbmsk & 0x7FFF) == 0x7FFF) cm &= 8; + else if (p->fbmsk & 0x7FFF) logonce(&lg_partial, "16-bit FBMSK (taken as written)"); + } + int zw = zt && zwrite; + /* passes: the alpha test's failing pixels get the AFAIL writes */ + struct { int apass, cm, zw; } ps[2]; int np = 0; + if (!p->ate || p->atst == 1) { ps[0].apass = 0; ps[0].cm = cm; ps[0].zw = zw; np = 1; } + else { + int fcm = cm, fzw = 0; + switch (p->afail) { + case 1: fcm = cm; fzw = 0; break; + case 2: fcm = 0; fzw = zw; break; + case 3: fcm = cm & 7; fzw = 0; break; + } + if (p->afail == 0) { ps[0].apass = 1; ps[0].cm = cm; ps[0].zw = zw; np = 1; } + else if (p->atst == 0) { ps[0].apass = 0; ps[0].cm = fcm; ps[0].zw = fzw; np = 1; } + else { ps[0].apass = 1; ps[0].cm = cm; ps[0].zw = zw; ps[1].apass = 2; ps[1].cm = fcm; ps[1].zw = fzw; np = 2; } + } + GLint U[4 * 10]; + { + int lin_mxl = p->mxl; + int zfmt = p->zpsm == 0x30 ? 1 : ((p->zpsm == 0x32 || p->zpsm == 0x3A) ? 2 : 0); + GLint u[40] = { + p->tme, p->tfx, p->tcc, p->fge, + p->ate && p->atst != 1, p->atst, p->aref, 0, + p->fba, date, p->datm, CLS_16(fc), + p->wms, p->wmt, p->minu, p->maxu, + p->minv, p->maxv, p->mmag, p->mmin, + lin_mxl, p->lcm, p->l, rt ? 1 : 0, + rt && BITS(p->tex0, 20, 6) == 1, (GLint)BITS(p->texa, 0, 8), (GLint)BITS(p->texa, 15, 1), p->iip || b_mode == GL_POINTS, + p->fogr, p->fogg, p->fogb, zfmt, + bla, bld, p->bc == 0 ? 0 : (p->bc == 2 ? 1 : 2), p->fix, + p->abe, rt ? rt->w : 1, rt ? rt->rows * rt->ph : 1, neg }; + memcpy(U, u, sizeof u); + } + + p_glUniform4f(u_vp, 2.0f / (float)ct->w, 2.0f / (float)TGH, 0.5f - 1.0f / 128.0f, (float)sc); /* (w: the point size) */ + int first; + { /* the vertices into a ring buffer: written ahead of what the GPU reads, orphaned when it wraps */ + static size_t off; + size_t n = sizeof(gv_t) * (size_t)nv, cap = (size_t)8 << 20; + p_glBindBuffer(GL_ARRAY_BUFFER, vbo); + GLbitfield fl = GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT; + if (off + n > cap) { p_glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)cap, 0, GL_STREAM_DRAW); off = 0; } + if (glt_q) { u64 *c = glt_cmd(GLT_VB, 1, n); c[0] = (u64)off; memcpy(c + 1, vb, n); (void)fl; } /* the GL thread maps and copies */ + else { + void *m = p_glMapBufferRange(GL_ARRAY_BUFFER, (GLintptr)off, (GLsizeiptr)n, fl); + if (m) { memcpy(m, vb, n); p_glUnmapBuffer(GL_ARRAY_BUFFER); } + } + first = (int)(off / sizeof(gv_t)); + off += n; + } + /* two passes (the alpha test's failing pixels): each run of one texture's + * primitives gets both before the next run, as the GS draws them in order */ + /* (runs that do not overlap the ones before them in a group go together: + * tiles of 32 pixels, 64 x 64) */ + int split = np > 1 && nrec > 1, u_sent = 0; + static u64 gt[64]; + for (int g = 0; g < (split ? nseg : 1);) { + int v0 = split ? seg_v[g] : 0, e = g + 1; + if (split) { + memset(gt, 0, sizeof gt); + for (; e <= nseg; e++) { + const float *b = seg_bb[e - 1]; + if (b[1] < b[0]) continue; + int tx0 = (int)b[0] >> 5, tx1 = (int)b[1] >> 5, ty0 = (int)b[2] >> 5, ty1 = (int)b[3] >> 5; + if (tx0 < 0) tx0 = 0; if (ty0 < 0) ty0 = 0; if (tx1 > 63) tx1 = 63; if (ty1 > 63) ty1 = 63; + u64 m = (tx1 >= tx0) ? ((tx1 - tx0 == 63) ? ~0ull : (((1ull << (tx1 - tx0 + 1)) - 1) << tx0)) : 0; + int hit = 0; + for (int y = ty0; y <= ty1; y++) if (gt[y] & m) hit = 1; + if (hit && e - 1 > g) { e--; break; } /* overlaps the group: it starts the next */ + for (int y = ty0; y <= ty1; y++) gt[y] |= m; + } + if (e > nseg) e = nseg; + if (e <= g) e = g + 1; + } + int v1 = (split && e < nseg) ? seg_v[e] : nv; + g = split ? e : 1; + if (v1 <= v0) continue; + for (int k = 0; k < np; k++) { + if (!u_sent) { p_glUniform4iv(u_i, 10, U); u_sent = 1; } + if (gc_apass != ps[k].apass) { p_glUniform1i(u_apass, ps[k].apass); gc_apass = ps[k].apass; } + if (gc.cmask != ps[k].cm) { p_glColorMask(ps[k].cm & 1, (ps[k].cm >> 1) & 1, (ps[k].cm >> 2) & 1, (ps[k].cm >> 3) & 1); gc.cmask = ps[k].cm; } + if (gc.dmask != ps[k].zw) { p_glDepthMask((GLboolean)ps[k].zw); gc.dmask = ps[k].zw; } + p_glDrawArrays((GLenum)b_mode, first + v0, v1 - v0); + st.draws++; + } + } + { /* testing: UYA_GLPIX=x,y,fbp(hex),from_vsync logs the batches that change that pixel of that target */ + static int on = -1; static int px_, py_; static unsigned fb_, from_; static u32 last; static float lastz; + if (on < 0) { const char *e = getenv("UYA_GLPIX"); on = e && sscanf(e, "%d,%d,%x,%u", &px_, &py_, &fb_, &from_) == 4; } + if (on && ct->bp == fb_ && uya_frame_count() >= from_ && px_ >= x0 && px_ <= x1 && py_ >= y0 && py_ <= y1) { + u32 v = 0; float z = -1; + p_glReadPixels(px_ * sc, py_ * sc, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &v); + if (zt) p_glReadPixels(px_ * sc, py_ * sc, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z); + uya_log("pix v%u %08X z %08X%s: n %d tme %u tex0 %llX abe %u ate %u/%u/%u/%u zt %u/%u (Z %s) passes %d v0 %.1f,%.1f z %08X", uya_frame_count(), v, (u32)(z * 16777216.0f), v != last || z != lastz ? " CHANGED" : "", + nv, p->tme, (unsigned long long)p->tex0, p->abe, p->ate, p->atst, p->aref, p->afail, p->zte, p->ztst, zt ? "on" : "off", np, vb[0].x, vb[0].y, vb[0].zr); + last = v; lastz = z; + for (int r = 0; r < nrec; r++) { const GLint *R = recs[r]; const pcmd_t *q = r ? &rec_st[r] : p; + uya_log(" rec %d: tex0 %llX tw %g th %g wm %d/%d %d-%d %d-%d mmag %d mmin %d mxl %d lcm %d tfx %d tcc %d L0 %d,%d %dx%d (%dx%d) defer %d", r, (unsigned long long)q->tex0, q->tw, q->th, + R[0], R[1], R[2], R[3], R[4], R[5], R[6], R[7], R[8], R[9], R[12], R[13], R[16], R[17], R[18], R[19], R[20], R[21], q->defer); } + } + } +done: + { /* UYA_GLDEBUG: GL errors after batches */ + static int dbg = -1, left = 20; + if (dbg < 0) dbg = getenv("UYA_GLDEBUG") != 0; + if (dbg && left > 0) { GLenum e = p_glGetError(); if (e) { left--; uya_log("GL GS: error %X after a batch (fpsm %X tme %u abe %u ate %u date %u rt %u)", e, p->fpsm, p->tme, p->abe, p->ate, p->date, p->rt); } } + } + nv = 0; + in_flush = 0; + gsgl_batch_id++; /* the batch's texture slots are free to go */ + if (glt_q) { glt_wr = glt_pend; glt_publish(); } /* the batch to the GL thread now */ +} + +/* ---- shaders ---- */ +static const char *vs_src = + "#version 330 core\n" + "layout(location=0) in vec2 a_pos; layout(location=1) in vec3 a_tex; layout(location=2) in uint a_z;\n" + "layout(location=3) in vec4 a_col; layout(location=4) in vec2 a_fd; layout(location=5) in float a_dz; layout(location=6) in uint a_rec;\n" + "uniform vec4 u_vp;\n" + "noperspective out vec4 v_col; flat out vec4 v_fcol; noperspective out vec3 v_tex; flat out float v_div;\n" + "noperspective out float v_z; noperspective out float v_f; flat out uint v_zr; flat out int v_rec;\n" + "void main() {\n" + " v_rec = int(a_rec);\n" + " vec2 p = a_pos + vec2(u_vp.z);\n" + " gl_Position = vec4(p.x * u_vp.x - 1.0, p.y * u_vp.y - 1.0, 0.0, 1.0);\n" + " gl_PointSize = u_vp.w;\n" + " v_col = a_col; v_fcol = a_col; v_tex = a_tex; v_div = a_fd.y; v_z = a_dz; v_zr = a_z; v_f = a_fd.x;\n" + "}\n"; +static const char *fs_src = + "#version 330 core\n" + "uniform usampler2D u_atlas;\n" + "uniform isamplerBuffer u_rec;\n" + "uniform sampler2D u_rt, u_dt;\n" + "uniform ivec4 u_i[10]; uniform int u_apass; uniform int u_sc;\n" + "noperspective in vec4 v_col; flat in vec4 v_fcol; noperspective in vec3 v_tex; flat in float v_div;\n" + "noperspective in float v_z; noperspective in float v_f; flat in uint v_zr; flat in int v_rec;\n" + "ivec4 R0, R1, R2, R3; int RB;\n" + "layout(location=0, index=0) out vec4 o_c;\n" + "layout(location=0, index=1) out vec4 o_b;\n" + "#define TME u_i[0].x\n#define TFX R3.x\n#define TCC R3.y\n#define FGE u_i[0].w\n" + "#define ATE u_i[1].x\n#define ATST u_i[1].y\n#define AREF u_i[1].z\n#define APASS u_apass\n" + "#define FBA u_i[2].x\n#define DATE u_i[2].y\n#define DATM u_i[2].z\n#define OUT16 u_i[2].w\n" + "#define WMS R0.x\n#define WMT R0.y\n#define MINU R0.z\n#define MAXU R0.w\n" + "#define MINV R1.x\n#define MAXV R1.y\n#define MMAG R1.z\n#define MMIN R1.w\n" + "#define MXL R2.x\n#define LCM R2.y\n#define LL R2.z\n#define KK intBitsToFloat(R2.w)\n#define SRC u_i[5].w\n" + "ivec4 LVP(int l) { return texelFetch(u_rec, RB + 4 + 2 * l); }\n" + "ivec4 LVS(int l) { return texelFetch(u_rec, RB + 5 + 2 * l); }\n" + "#define RT24 u_i[6].x\n#define TA0 u_i[6].y\n#define AEM u_i[6].z\n#define IIP u_i[6].w\n" + "#define FOGC u_i[7].xyz\n#define ZFMT u_i[7].w\n" + "#define BLA u_i[8].x\n#define BLD u_i[8].y\n#define BLC u_i[8].z\n#define FIX u_i[8].w\n" + "#define ABE u_i[9].x\n#define RTW u_i[9].y\n#define RTH u_i[9].z\n#define BNEG u_i[9].w\n" + "int wrap1(int x, int n, int wm, int mn, int mx) {\n" + " if (wm == 0) return x & (n - 1);\n" + " if (wm == 1) return x < 0 ? 0 : (x >= n ? n - 1 : x);\n" + " if (wm == 2) return x < mn ? mn : (x > mx ? mx : x);\n" + " return (x & mn) | mx;\n" + "}\n" + "uvec4 rawfetch(int l, ivec2 c) {\n" + " if (SRC == 1) {\n" + " c = clamp(c, ivec2(0), ivec2(RTW - 1, RTH - 1));\n" + " uvec4 t = uvec4(texelFetch(u_rt, c * u_sc + u_sc / 2, 0) * 255.0 + 0.5);\n" + " if (RT24 != 0) t.a = (AEM != 0 && t.r + t.g + t.b == 0u) ? 0u : uint(TA0);\n" + " return t;\n" + " }\n" + " ivec4 lp = LVP(l);\n" + " c = clamp(c, ivec2(0), lp.zw - 1);\n" + " return texelFetch(u_atlas, lp.xy + c, 0);\n" + "}\n" + "uvec4 rtfetch_s(ivec2 c) {\n" /* a colour target read at its upscaled resolution (clamping wrap modes) */ + " ivec2 lo = ivec2(WMS == 2 ? MINU * u_sc : 0, WMT == 2 ? MINV * u_sc : 0);\n" + " ivec2 hi = min(ivec2(WMS == 2 ? MAXU * u_sc + u_sc - 1 : RTW * u_sc - 1, WMT == 2 ? MAXV * u_sc + u_sc - 1 : RTH * u_sc - 1), ivec2(RTW * u_sc - 1, RTH * u_sc - 1));\n" + " uvec4 t = uvec4(texelFetch(u_rt, clamp(c, lo, hi), 0) * 255.0 + 0.5);\n" + " if (RT24 != 0) t.a = (AEM != 0 && t.r + t.g + t.b == 0u) ? 0u : uint(TA0);\n" + " return t;\n" + "}\n" + "uvec4 tfetch(int l, int x, int y) {\n" + " ivec4 ls = LVS(l);\n" + " return rawfetch(l, ivec2(wrap1(x, ls.x, WMS, MINU, MAXU), wrap1(y, ls.y, WMT, MINV, MAXV)));\n" + "}\n" + "uvec4 tsample_lvl(int l, float u, float v, bool lin) {\n" + " if (SRC == 1 && l == 0 && u_sc > 1 && (WMS == 1 || WMS == 2) && (WMT == 1 || WMT == 2)) {\n" + " float su = u * float(u_sc), sv = v * float(u_sc);\n" + " if (!lin) return rtfetch_s(ivec2(floor(su), floor(sv)));\n" + " su -= 0.5; sv -= 0.5;\n" + " float gx = floor(su), gy = floor(sv); ivec2 g = ivec2(gx, gy);\n" + " uint gu = uint(int((su - gx) * 256.0)), gv = uint(int((sv - gy) * 256.0));\n" + " uvec4 q0 = (rtfetch_s(g) * (256u - gu) + rtfetch_s(g + ivec2(1, 0)) * gu) >> 8;\n" + " uvec4 q1 = (rtfetch_s(g + ivec2(0, 1)) * (256u - gu) + rtfetch_s(g + ivec2(1, 1)) * gu) >> 8;\n" + " return (q0 * (256u - gv) + q1 * gv) >> 8;\n" + " }\n" + " if (!lin) return tfetch(l, int(floor(u)), int(floor(v)));\n" + " u -= 0.5; v -= 0.5;\n" + " float fx = floor(u), fy = floor(v);\n" + " int x0 = int(fx), y0 = int(fy);\n" + " uint fu = uint(int((u - fx) * 256.0)), fv = uint(int((v - fy) * 256.0));\n" + " uvec4 p00 = tfetch(l, x0, y0), p10 = tfetch(l, x0 + 1, y0), p01 = tfetch(l, x0, y0 + 1), p11 = tfetch(l, x0 + 1, y0 + 1);\n" + " uvec4 r0 = (p00 * (256u - fu) + p10 * fu) >> 8;\n" + " uvec4 r1 = (p01 * (256u - fu) + p11 * fu) >> 8;\n" + " return (r0 * (256u - fv) + r1 * fv) >> 8;\n" + "}\n" + "uvec4 tsample(float u, float v, float q) {\n" + " bool lin = (MMIN == 1 || MMIN == 4 || MMIN == 5);\n" + " if ((MXL == 0 || MMIN < 2) && lin == (MMAG != 0)) return tsample_lvl(0, u, v, lin);\n" + " float lod;\n" + " if (LCM != 0) lod = KK;\n" + " else { float aq = abs(q); lod = (aq > 0.0 ? -log2(aq) : 126.0) * float(1 << LL) + KK; }\n" + " lod = clamp(lod, -64.0, 64.0);\n" + " lod = floor(lod * 16.0 + 0.5) * (1.0 / 16.0);\n" + " if (lod <= 0.0 || MXL == 0 || MMIN < 2) return tsample_lvl(0, u, v, lod <= 0.0 ? (MMAG != 0) : lin);\n" + " if (MMIN == 3 || MMIN == 5) {\n" + " int l0 = int(floor(lod)); float fr = lod - float(l0);\n" + " if (l0 >= MXL) { float s = 1.0 / float(1 << MXL); return tsample_lvl(MXL, u * s, v * s, lin); }\n" + " float s0 = 1.0 / float(1 << l0), s1 = 1.0 / float(2 << l0);\n" + " uvec4 a = tsample_lvl(l0, u * s0, v * s0, lin), b = tsample_lvl(l0 + 1, u * s1, v * s1, lin);\n" + " uint f = uint(fr * 256.0);\n" + " return (a * (256u - f) + b * f) >> 8;\n" + " }\n" + " int lvl = int(floor(lod + 0.5)); if (lvl > MXL) lvl = MXL;\n" + " if (lvl == 0) return tsample_lvl(0, u, v, lin);\n" + " float s = 1.0 / float(1 << lvl);\n" + " return tsample_lvl(lvl, u * s, v * s, lin);\n" + "}\n" + "void main() {\n" + " RB = v_rec * 18; R0 = texelFetch(u_rec, RB); R1 = texelFetch(u_rec, RB + 1); R2 = texelFetch(u_rec, RB + 2); R3 = texelFetch(u_rec, RB + 3);\n" + " ivec4 c = IIP != 0 ? ivec4(v_col - (dFdx(v_col) + dFdy(v_col)) * (float(u_sc) / 128.0)) : ivec4(v_fcol);\n" + " if (TME != 0) {\n" + " vec3 tx = v_tex - (dFdx(v_tex) + dFdy(v_tex)) * (float(u_sc) / 128.0); /* at the GS's sample point (the vertices are 1/128 pixel off) */\n" + " float u = tx.x, v = tx.y, q = tx.z;\n" + " if (v_div != 0.0 && q != 0.0) { float iq = 1.0 / q; u *= iq; v *= iq; }\n" + " ivec4 t = ivec4(tsample(u, v, q));\n" + " if (TFX == 0) { c.rgb = (t.rgb * c.rgb) >> 7; if (TCC != 0) c.a = (t.a * c.a) >> 7; }\n" + " else if (TFX == 1) { c.rgb = t.rgb; if (TCC != 0) c.a = t.a; }\n" + " else if (TFX == 2) { c.rgb = ((t.rgb * c.rgb) >> 7) + c.a; if (TCC != 0) c.a = t.a + c.a; }\n" + " else { c.rgb = ((t.rgb * c.rgb) >> 7) + c.a; if (TCC != 0) c.a = t.a; }\n" + " c = clamp(c, 0, 255);\n" + " }\n" + " if (FGE != 0) { int f = int(v_f); c.rgb = (c.rgb * f + FOGC * (255 - f)) >> 8; }\n" + " if (ATE != 0) {\n" + " bool pass;\n" + " if (ATST == 0) pass = false; else if (ATST == 1) pass = true;\n" + " else if (ATST == 2) pass = c.a < AREF; else if (ATST == 3) pass = c.a <= AREF;\n" + " else if (ATST == 4) pass = c.a == AREF; else if (ATST == 5) pass = c.a >= AREF;\n" + " else if (ATST == 6) pass = c.a > AREF; else pass = c.a != AREF;\n" + " if (APASS == 1 && !pass) discard;\n" + " if (APASS == 2 && pass) discard;\n" + " }\n" + " if (DATE != 0) {\n" + " int da = int(texelFetch(u_dt, ivec2(gl_FragCoord.xy), 0).a * 255.0 + 0.5);\n" + " if (((da >> 7) & 1) != DATM) discard;\n" + " }\n" + " float zf = v_z - (dFdx(v_z) + dFdy(v_z)) * (float(u_sc) / 128.0);\n" + " uint zi = v_zr + (zf <= 0.0 ? 0u : uint(min(zf, 4294967040.0)));\n" + " if (ZFMT == 0) gl_FragDepth = float(zi & 0xFFFFFFu) * (1.0 / 16777216.0);\n" + " else if (ZFMT == 1) gl_FragDepth = float(zi) * (1.0 / 4294967296.0);\n" + " else gl_FragDepth = float(min(zi, 0xFFFFu)) * (1.0 / 65536.0);\n" + " int cv = BLC == 0 ? c.a : (BLC == 1 ? FIX : 128);\n" + " ivec3 s = c.rgb;\n" + " if (ABE != 0) { s = ((BLA * c.rgb * cv) >> 7) + BLD * c.rgb; if (BNEG != 0) s = -s; }\n" + " s = clamp(s, 0, 255);\n" + " int a = c.a;\n" + " if (FBA != 0) a |= 0x80;\n" + " if (OUT16 != 0) { s &= 0xF8; a &= 0x80; }\n" + " o_c = vec4(vec3(s), float(a)) * (1.0 / 255.0);\n" + " o_b = vec4(0.0, 0.0, 0.0, min(float(cv), 128.0) / 128.0);\n" + "}\n"; + +static const char *pvs_src = + "#version 330 core\n" + "out vec2 v_uv;\n" + "void main() { vec2 p = vec2((gl_VertexID & 1) != 0 ? 1.0 : 0.0, (gl_VertexID & 2) != 0 ? 1.0 : 0.0); v_uv = p; gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }\n"; +static const char *pfs_src = + "#version 330 core\n" + "uniform sampler2D u_tex; uniform vec4 u_src; uniform vec4 u_lim; uniform vec2 u_tsz; uniform ivec2 u_mode; uniform int u_ps;\n" + "in vec2 v_uv; out vec4 o;\n" + "void main() {\n" + " vec2 uv = v_uv; if (u_mode.x != 0) uv.y = 1.0 - uv.y;\n" + " vec2 f = uv * u_lim.zw - u_lim.xy;\n" + " if (f.x < 0.0 || f.y < 0.0 || f.x >= u_lim.z || f.y >= u_lim.w) { o = vec4(0.0, 0.0, 0.0, 1.0); return; }\n" + " vec2 t = u_src.xy + f;\n" + " vec4 c = u_mode.y != 0 ? texelFetch(u_tex, ivec2(floor(t)) * u_ps + u_ps / 2, 0) : texture(u_tex, t / u_tsz);\n" + " o = vec4(c.rgb, 1.0);\n" + "}\n"; + +static GLuint compile(GLenum type, const char *src) +{ + GLuint s = p_glCreateShader(type); + p_glShaderSource(s, 1, &src, 0); + p_glCompileShader(s); + GLint ok = 0; p_glGetShaderiv(s, GL_COMPILE_STATUS, &ok); + if (!ok) { char log[4096]; p_glGetShaderInfoLog(s, sizeof log, 0, log); uya_log("GL GS: shader error: %s", log); return 0; } + return s; +} +static GLuint link2(GLuint v, GLuint f, int dual) +{ + if (!v || !f) return 0; + GLuint pr = p_glCreateProgram(); + p_glAttachShader(pr, v); p_glAttachShader(pr, f); + if (dual) { p_glBindFragDataLocationIndexed(pr, 0, 0, "o_c"); p_glBindFragDataLocationIndexed(pr, 0, 1, "o_b"); } + p_glLinkProgram(pr); + GLint ok = 0; p_glGetProgramiv(pr, GL_LINK_STATUS, &ok); + if (!ok) { char log[4096]; p_glGetProgramInfoLog(pr, sizeof log, 0, log); uya_log("GL GS: link error: %s", log); return 0; } + return pr; +} + +int gsgl_init(u32 *vr) +{ + vram = vr; + { /* the upscale (UYA_SCALE=1..4; the launcher picks it from the screen) */ + const char *e = getenv("UYA_SCALE"); + sc = e ? atoi(e) : 1; + if (sc < 1) sc = 1; + if (sc > 4) sc = 4; + } + gs_swizzle_init(); +#define GSGL_LOAD(name, ret, args) r_##name = (PFN_##name)plat_gl_proc(#name); if (!r_##name) { uya_log("GL GS: no %s", #name); return 0; } + GSGL_FUNCS(GSGL_LOAD) +#undef GSGL_LOAD + const char *ver = (const char *)p_glGetString(GL_VERSION); + uya_log("GL GS: %s | %s | %s", (const char *)p_glGetString(GL_VENDOR), (const char *)p_glGetString(GL_RENDERER), ver ? ver : "?"); + prog = link2(compile(GL_VERTEX_SHADER, vs_src), compile(GL_FRAGMENT_SHADER, fs_src), 1); + prog_p = link2(compile(GL_VERTEX_SHADER, pvs_src), compile(GL_FRAGMENT_SHADER, pfs_src), 0); + if (!prog || !prog_p) return 0; + p_glUseProgram(prog); + { + p_glUniform1i(p_glGetUniformLocation(prog, "u_atlas"), 0); + p_glUniform1i(p_glGetUniformLocation(prog, "u_rt"), 7); + p_glUniform1i(p_glGetUniformLocation(prog, "u_dt"), 8); + p_glUniform1i(p_glGetUniformLocation(prog, "u_rec"), 9); + p_glUniform1i(p_glGetUniformLocation(prog, "u_sc"), sc); + } + u_i = p_glGetUniformLocation(prog, "u_i"); u_vp = p_glGetUniformLocation(prog, "u_vp"); u_apass = p_glGetUniformLocation(prog, "u_apass"); + p_glUseProgram(prog_p); + p_glUniform1i(p_glGetUniformLocation(prog_p, "u_tex"), 0); + up_src = p_glGetUniformLocation(prog_p, "u_src"); up_lim = p_glGetUniformLocation(prog_p, "u_lim"); + up_tsz = p_glGetUniformLocation(prog_p, "u_tsz"); up_mode = p_glGetUniformLocation(prog_p, "u_mode"); up_ps = p_glGetUniformLocation(prog_p, "u_ps"); + p_glGenVertexArrays(1, &vao); + p_glBindVertexArray(vao); + p_glGenBuffers(1, &vbo); + p_glBindBuffer(GL_ARRAY_BUFFER, vbo); + p_glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)8 << 20, 0, GL_STREAM_DRAW); + p_glEnableVertexAttribArray(0); p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(gv_t), (void *)0); + p_glEnableVertexAttribArray(1); p_glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(gv_t), (void *)8); + p_glEnableVertexAttribArray(2); p_glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, sizeof(gv_t), (void *)24); + p_glEnableVertexAttribArray(3); p_glVertexAttribPointer(3, 4, GL_UNSIGNED_BYTE, GL_FALSE, sizeof(gv_t), (void *)28); + p_glEnableVertexAttribArray(4); p_glVertexAttribPointer(4, 2, GL_UNSIGNED_BYTE, GL_FALSE, sizeof(gv_t), (void *)32); + p_glEnableVertexAttribArray(5); p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, sizeof(gv_t), (void *)20); + p_glEnableVertexAttribArray(6); p_glVertexAttribIPointer(6, 1, GL_UNSIGNED_SHORT, sizeof(gv_t), (void *)34); + atlas_tex = mk_tex(GL_RGBA8UI, ATLAS, ATLAS, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, 0); + p_glGenBuffers(1, &rec_buf); + p_glBindBuffer(GL_TEXTURE_BUFFER, rec_buf); + p_glBufferData(GL_TEXTURE_BUFFER, (GLsizeiptr)sizeof recs, 0, GL_STREAM_DRAW); + p_glGenTextures(1, &rec_tex); + p_glActiveTexture(GL_TEXTURE0 + 9); + p_glBindTexture(GL_TEXTURE_BUFFER, rec_tex); + p_glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32I, rec_buf); + p_glBindBuffer(GL_ARRAY_BUFFER, vbo); + p_glGenVertexArrays(1, &vao_p); + { + static const u8 z4[4] = {0, 0, 0, 0}; + dummy_ui = mk_tex(GL_RGBA8UI, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, 0); + p_glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, z4); + dummy_f = mk_tex(GL_RGBA8, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, 0); + p_glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, z4); + } + p_glGenFramebuffers(1, &rd_fbo); + p_glGenFramebuffers(1, &rd_fbo_z); + p_glBindFramebuffer(GL_FRAMEBUFFER, rd_fbo_z); + p_glDrawBuffer(GL_NONE_); p_glReadBuffer(GL_NONE_); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + p_glDisable(GL_CULL_FACE); + p_glDisable(GL_DITHER); + p_glEnable(0x8642 /* GL_PROGRAM_POINT_SIZE: points (lines, dots) cover their upscaled pixel */); + p_glProvokingVertex(GL_LAST_VERTEX_CONVENTION); + memset(owner, -1, sizeof owner); + gc_reset(); + GLenum e = p_glGetError(); + if (e) { uya_log("GL GS: GL error %X at init", e); return 0; } + if (sc > 1) uya_log("GL GS: drawing at %dx the PS2's resolution", sc); + gsgl_on = 1; + glt_start(); + return 1; +} + +/* ---- display ---- */ +static void present_draw(GLuint tex, const gs_disp_t *d, int tw, int vx, int vy, int vw, int vh, int flip, int nearest, int ps) +{ + p_glDisable(GL_SCISSOR_TEST); p_glDisable(GL_BLEND); p_glDisable(GL_DEPTH_TEST); + p_glColorMask(1, 1, 1, 1); + p_glViewport(vx, vy, vw, vh); + p_glUseProgram(prog_p); + p_glBindVertexArray(vao_p); + p_glActiveTexture(GL_TEXTURE0); + p_glBindTexture(GL_TEXTURE_2D, tex); + p_glUniform4f(up_src, (float)d->dbx, (float)d->dby, 0, 0); + p_glUniform4f(up_lim, (float)d->shx, (float)d->shy, (float)d->w, (float)d->h); + p_glUniform2f(up_tsz, (float)tw, (float)TGH); + p_glUniform2i(up_mode, flip, nearest); + p_glUniform1i(up_ps, ps); + p_glDrawArrays(0x0005 /* GL_TRIANGLE_STRIP */, 0, 4); + gc_reset(); +} +static tgt_t *disp_tgt(const gs_disp_t *d) +{ + int c = cls_of(d->psm); + if (c < 0 || CLS_Z(c) || !d->fbw) return 0; + tgt_t *t = tgt_get(d->fbp, d->fbw, c, 0, 0); + ensure_valid(t, (int)d->dbx, (int)d->dby, (int)(d->dbx + d->w - 1), (int)(d->dby + d->h - 1)); + return t; +} +void gsgl_stats(int n) +{ + double k = n > 0 ? n : 1; + uya_log("GL GS (per vsync): %.0f prims, %.0f batches, %.0f draws, %.1f texture uploads (%.1f late), %.2f pages down (%.2f reads), %.2f up, %.2f copies, %.2f atlas resets", + (st.prims - st0.prims) / k, (st.flushes - st0.flushes) / k, (st.draws - st0.draws) / k, (st.tex_ul - st0.tex_ul) / k, + (st.late - st0.late) / k, (st.dl_pages - st0.dl_pages) / k, (st.dl_reads - st0.dl_reads) / k, (st.ul_pages - st0.ul_pages) / k, + (st.copies - st0.copies) / k, (st.atlas_resets - st0.atlas_resets) / k); + st0 = st; +} +void gsgl_present(const gs_disp_t *d) +{ + gsgl_flush(); + static unsigned n; + static int fpslog = -1; + if (fpslog < 0) fpslog = getenv("UYA_FPSLOG") != 0; + if (fpslog && ++n % 100 == 0) gsgl_stats(100); + int ww = 640, wh = 480; + plat_gl_size(&ww, &wh); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + gc.fbo = ~0u; + p_glDisable(GL_SCISSOR_TEST); + p_glColorMask(1, 1, 1, 1); + p_glClearColor(0, 0, 0, 1); + p_glClear(GL_COLOR_BUFFER_BIT); + tgt_t *t = d ? disp_tgt(d) : 0; + if (t) { + int vw = ww, vh = ww * 3 / 4; /* 4:3, black bars around */ + if (vh > wh) { vh = wh; vw = wh * 4 / 3; } + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + present_draw(t->tex, d, t->w, (ww - vw) / 2, (wh - vh) / 2, vw, vh, 1, 0, sc); + } + { /* testing: UYA_GLDUMP=dir[,n] saves every nth window picture (default 50) as dir/gl_NNNNN.ppm */ + static int on = -1, every = 50; static char dir[512]; static unsigned k; + if (on < 0) { const char *e = getenv("UYA_GLDUMP"); on = 0; if (e) { on = 1; snprintf(dir, sizeof dir, "%s", e); char *c = strchr(dir, ','); if (c) { *c = 0; every = atoi(c + 1); if (every < 1) every = 1; } } } + if (on && k++ % (unsigned)every == 0) { + u8 *px = malloc((size_t)ww * wh * 4); + p_glReadBuffer(GL_BACK); p_glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, px); + char path[600]; snprintf(path, sizeof path, "%s/gl_%05u.ppm", dir, k - 1); + FILE *f = fopen(path, "wb"); + if (f) { fprintf(f, "P6 %d %d 255\n", ww, wh); for (int y = wh - 1; y >= 0; y--) for (int x = 0; x < ww; x++) fwrite(px + ((size_t)y * ww + x) * 4, 1, 3, f); fclose(f); } + free(px); + } + } + gc_reset(); + glt_swap(); +} +/* a picture from the CPU (an FMV frame: BGRX rows, top first) to the window */ +void gsgl_present_bgra(const u32 *px, int w, int h) +{ + static GLuint mtex; + if (w <= 0 || h <= 0 || w > 1024 || h > TGH) return; + gsgl_flush(); + if (!mtex) mtex = mk_tex(GL_RGBA8, 1024, TGH, GL_RGBA, GL_UNSIGNED_BYTE, 1); + p_glActiveTexture(GL_TEXTURE0); + p_glBindTexture(GL_TEXTURE_2D, mtex); + p_glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + p_glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, px); + int ww = 640, wh = 480; + plat_gl_size(&ww, &wh); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + gc.fbo = ~0u; + p_glDisable(GL_SCISSOR_TEST); + p_glColorMask(1, 1, 1, 1); + p_glClearColor(0, 0, 0, 1); + p_glClear(GL_COLOR_BUFFER_BIT); + gs_disp_t d; memset(&d, 0, sizeof d); + d.w = (u32)w; d.h = (u32)h; + int vw = ww, vh = ww * 3 / 4; + if (vh > wh) { vh = wh; vw = wh * 4 / 3; } + present_draw(mtex, &d, 1024, (ww - vw) / 2, (wh - vh) / 2, vw, vh, 1, 0, 1); + gc_reset(); + glt_swap(); +} +int gsgl_capture(const gs_disp_t *d, u32 *out) +{ + gsgl_flush(); + tgt_t *t = disp_tgt(d); + if (!t) return 0; + if (!cap_tex) { + cap_tex = mk_tex(GL_RGBA8, 1024, 1024, GL_RGBA, GL_UNSIGNED_BYTE, 0); + p_glGenFramebuffers(1, &cap_fbo); + p_glBindFramebuffer(GL_FRAMEBUFFER, cap_fbo); + p_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cap_tex, 0); + } + p_glBindFramebuffer(GL_FRAMEBUFFER, cap_fbo); + present_draw(t->tex, d, t->w, 0, 0, (int)d->w, (int)d->h, 0, 1, sc); + p_glBindFramebuffer(GL_FRAMEBUFFER, cap_fbo); + p_glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glReadPixels(0, 0, (GLsizei)d->w, (GLsizei)d->h, GL_RGBA, GL_UNSIGNED_BYTE, out); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + gc_reset(); + for (u32 i = 0; i < d->w * d->h; i++) { u32 v = out[i]; out[i] = ((v & 255) << 16) | (v & 0xFF00) | ((v >> 16) & 255); } + return 1; +} +/* testing (tools/gs_replay.c REPLAY_FULL): the displayed picture at the upscaled resolution + * (w*sc x h*sc, RGBA rows bottom first); out must hold 4096 x 4096 pixels */ +int gsgl_capture_full(const gs_disp_t *d, u32 *out, int *ow, int *oh) +{ + static GLuint ftex, ffbo; + gsgl_flush(); + tgt_t *t = disp_tgt(d); + if (!t) return 0; + int w = (int)d->w * sc, h = (int)d->h * sc; + if (w > 4096 || h > 4096) return 0; + if (!ftex) { + ftex = mk_tex(GL_RGBA8, 4096, 4096, GL_RGBA, GL_UNSIGNED_BYTE, 0); + p_glGenFramebuffers(1, &ffbo); + p_glBindFramebuffer(GL_FRAMEBUFFER, ffbo); + p_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ftex, 0); + } + p_glBindFramebuffer(GL_FRAMEBUFFER, ffbo); + present_draw(t->tex, d, t->w, 0, 0, w, h, 0, 0, sc); + p_glBindFramebuffer(GL_FRAMEBUFFER, ffbo); + p_glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, out); + p_glBindFramebuffer(GL_FRAMEBUFFER, 0); + gc_reset(); + *ow = w; *oh = h; + return 1; +} diff --git a/games/rac1/host/src/gs/gs_gl.h b/games/rac1/host/src/gs/gs_gl.h new file mode 100644 index 0000000..2de8fda --- /dev/null +++ b/games/rac1/host/src/gs/gs_gl.h @@ -0,0 +1,56 @@ +/* gs_gl.h: the GPU (OpenGL 3.3 core) renderer beside the software GS. + * + * gs.c keeps the GIF front end, the registers, GS local memory, transfers, + * CLUTs and the texture cache; with gsgl_on its primitives go to gs_gl.c, + * which draws them on the GPU into GL textures standing for GS frame and Z + * buffers (targets), and keeps GS memory and the targets coherent: whatever + * the CPU side reads or writes in memory a target covers is synced first + * (gsgl_cpu_read / gsgl_cpu_write). All calls are made on the thread that + * owns the GL context (the GS thread with MTGS). */ +#ifndef GS_GL_H +#define GS_GL_H +#include "gs_types.h" + +extern int gsgl_on; /* the GL renderer draws */ + +/* the display circuits' picture (from PMODE, SMODE2, DISPFB, DISPLAY) */ +typedef struct { u32 fbp, fbw, psm, dbx, dby, w, h; s32 shx, shy; } gs_disp_t; + +int gsgl_init(u32 *vram); /* with a current GL 3.3 context: 1 if the renderer can run */ +void gsgl_prim(const pcmd_t *st, u32 st_index, const pprim_t *pr); +void gsgl_flush(void); /* draw the batched primitives */ + +/* textures (cmd_setup): 1 if the texture is read from a GPU target directly + * (p->rt set); else whether a level's memory is newer on the GPU (it is then + * decoded only if a batch needs it: gs_tex_late); a decoded level in use */ +int gsgl_tex_setup(pcmd_t *p); +int gsgl_tex_gpu(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th); +void gsgl_tex_touch(const tlevel_t *l); +void gsgl_clut_changed(void); + +/* GS memory the CPU side is about to read or write (a rectangle of a buffer, + * or a range of 256-byte blocks) */ +void gsgl_cpu_read_rect(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h); +void gsgl_cpu_write_rect(u32 psm, u32 bp, u32 bw, u32 x, u32 y, u32 w, u32 h); +void gsgl_cpu_read(u32 bp, u32 nblocks); +int gsgl_threaded(void); +void gsgl_thread_times(double *busy, double *wait); /* GL calls go to the GL thread (it owns the context) */ +int gsgl_tex_owned(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th); +void gsgl_tex_read(u32 psm, u32 tbp, u32 tbw, u32 tw, u32 th); + +/* the display: to the window (and swap), or read back as BGRX (testing) */ +void gsgl_present(const gs_disp_t *d); +void gsgl_present_bgra(const u32 *px, int w, int h); /* an FMV frame */ +int gsgl_capture(const gs_disp_t *d, u32 *out); +void gsgl_stats(int nvsyncs); /* counts per vsync since the last call */ + +/* gs.c: decode a level of a state's texture now */ +int gs_tex_late(const pcmd_t *p, int lvl, tlevel_t *out); + +/* the platform: a GL 3.3 core context for the window */ +int plat_gl_init(void); /* creates it, current on the calling thread */ +void plat_gl_current(int on); /* make it current on / release it from this thread */ +void plat_gl_swap(void); +void plat_gl_size(int *w, int *h); /* the window's drawable size */ +extern int plat_gl_active; /* presenting with GL (no GDI drawing) */ +#endif diff --git a/games/rac1/host/src/gs/gs_gl_api.h b/games/rac1/host/src/gs/gs_gl_api.h new file mode 100644 index 0000000..3518251 --- /dev/null +++ b/games/rac1/host/src/gs/gs_gl_api.h @@ -0,0 +1,193 @@ +/* gs_gl_api.h: the OpenGL 3.3 core declarations the GL renderer uses. No + * system GL headers: the entry points are loaded at run time through + * plat_gl_proc (wglGetProcAddress on Windows, eglGetProcAddress in the + * Linux test harness), so nothing links against a GL library. */ +#ifndef GS_GL_API_H +#define GS_GL_API_H +#include +#include + +#ifdef _WIN32 +#define GLAPIENTRY __stdcall +#else +#define GLAPIENTRY +#endif + +typedef unsigned int GLenum, GLuint, GLbitfield; +typedef int GLint, GLsizei; +typedef unsigned char GLboolean, GLubyte; +typedef float GLfloat; +typedef double GLdouble; +typedef char GLchar; +typedef ptrdiff_t GLsizeiptr, GLintptr; +typedef struct __GLsync *GLsync; +typedef uint64_t GLuint64; + +#define GL_NONE_ 0 +#define GL_FALSE 0 +#define GL_TRUE 1 +#define GL_NO_ERROR 0 +#define GL_ZERO 0 +#define GL_ONE 1 +#define GL_POINTS 0x0000 +#define GL_LINES 0x0001 +#define GL_TRIANGLES 0x0004 +#define GL_NEVER 0x0200 +#define GL_LESS 0x0201 +#define GL_EQUAL 0x0202 +#define GL_LEQUAL 0x0203 +#define GL_GREATER 0x0204 +#define GL_NOTEQUAL 0x0205 +#define GL_GEQUAL 0x0206 +#define GL_ALWAYS 0x0207 +#define GL_SRC_COLOR 0x0300 +#define GL_ONE_MINUS_SRC_COLOR 0x0301 +#define GL_SRC_ALPHA 0x0302 +#define GL_ONE_MINUS_SRC_ALPHA 0x0303 +#define GL_DST_ALPHA 0x0304 +#define GL_ONE_MINUS_DST_ALPHA 0x0305 +#define GL_CONSTANT_ALPHA 0x8003 +#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004 +#define GL_SRC1_ALPHA 0x8589 +#define GL_ONE_MINUS_SRC1_ALPHA 0x88FB +#define GL_FUNC_ADD 0x8006 +#define GL_FUNC_SUBTRACT 0x800A +#define GL_FUNC_REVERSE_SUBTRACT 0x800B +#define GL_DEPTH_TEST 0x0B71 +#define GL_BLEND 0x0BE2 +#define GL_SCISSOR_TEST 0x0C11 +#define GL_CULL_FACE 0x0B44 +#define GL_DITHER 0x0BD0 +#define GL_UNPACK_ALIGNMENT 0x0CF5 +#define GL_PACK_ALIGNMENT 0x0D05 +#define GL_UNPACK_ROW_LENGTH 0x0CF2 +#define GL_PACK_ROW_LENGTH 0x0D02 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_UNSIGNED_BYTE 0x1401 +#define GL_UNSIGNED_INT 0x1405 +#define GL_FLOAT 0x1406 +#define GL_DEPTH_COMPONENT 0x1902 +#define GL_RED 0x1903 +#define GL_RGBA 0x1908 +#define GL_BGRA 0x80E1 +#define GL_RGBA8 0x8058 +#define GL_RGBA8UI 0x8D7C +#define GL_TEXTURE_BUFFER 0x8C2A +#define GL_UNSIGNED_SHORT 0x1403 +#define GL_RGBA32I 0x8D82 +#define GL_RGBA_INTEGER 0x8D99 +#define GL_DEPTH_COMPONENT32F 0x8CAC +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 +#define GL_NEAREST 0x2600 +#define GL_LINEAR 0x2601 +#define GL_TEXTURE_MAG_FILTER 0x2800 +#define GL_TEXTURE_MIN_FILTER 0x2801 +#define GL_TEXTURE_WRAP_S 0x2802 +#define GL_TEXTURE_WRAP_T 0x2803 +#define GL_CLAMP_TO_EDGE 0x812F +#define GL_TEXTURE_MAX_LEVEL 0x813D +#define GL_COLOR_BUFFER_BIT 0x00004000 +#define GL_DEPTH_BUFFER_BIT 0x00000100 +#define GL_TEXTURE0 0x84C0 +#define GL_ARRAY_BUFFER 0x8892 +#define GL_STREAM_DRAW 0x88E0 +#define GL_DYNAMIC_DRAW 0x88E8 +#define GL_FRAGMENT_SHADER 0x8B30 +#define GL_VERTEX_SHADER 0x8B31 +#define GL_COMPILE_STATUS 0x8B81 +#define GL_LINK_STATUS 0x8B82 +#define GL_INFO_LOG_LENGTH 0x8B84 +#define GL_FRAMEBUFFER 0x8D40 +#define GL_READ_FRAMEBUFFER 0x8CA8 +#define GL_DRAW_FRAMEBUFFER 0x8CA9 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_DEPTH_ATTACHMENT 0x8D00 +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_MAP_WRITE_BIT 0x0002 +#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008 +#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020 +#define GL_FIRST_VERTEX_CONVENTION 0x8E4D +#define GL_LAST_VERTEX_CONVENTION 0x8E4E +#define GL_BACK 0x0405 +#define GL_PROGRAM_POINT_SIZE 0x8642 + +/* name, return type, arguments */ +#define GSGL_FUNCS(X) \ + X(glGetError, GLenum, (void)) \ + X(glGetString, const GLubyte *, (GLenum)) \ + X(glGetIntegerv, void, (GLenum, GLint *)) \ + X(glEnable, void, (GLenum)) \ + X(glDisable, void, (GLenum)) \ + X(glViewport, void, (GLint, GLint, GLsizei, GLsizei)) \ + X(glScissor, void, (GLint, GLint, GLsizei, GLsizei)) \ + X(glClearColor, void, (GLfloat, GLfloat, GLfloat, GLfloat)) \ + X(glClear, void, (GLbitfield)) \ + X(glColorMask, void, (GLboolean, GLboolean, GLboolean, GLboolean)) \ + X(glDepthMask, void, (GLboolean)) \ + X(glDepthFunc, void, (GLenum)) \ + X(glBlendFuncSeparate, void, (GLenum, GLenum, GLenum, GLenum)) \ + X(glBlendEquationSeparate, void, (GLenum, GLenum)) \ + X(glBlendColor, void, (GLfloat, GLfloat, GLfloat, GLfloat)) \ + X(glPixelStorei, void, (GLenum, GLint)) \ + X(glReadPixels, void, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void *)) \ + X(glGenTextures, void, (GLsizei, GLuint *)) \ + X(glDeleteTextures, void, (GLsizei, const GLuint *)) \ + X(glBindTexture, void, (GLenum, GLuint)) \ + X(glActiveTexture, void, (GLenum)) \ + X(glTexImage2D, void, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void *)) \ + X(glTexSubImage2D, void, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void *)) \ + X(glTexParameteri, void, (GLenum, GLenum, GLint)) \ + X(glDrawArrays, void, (GLenum, GLint, GLsizei)) \ + X(glFlush, void, (void)) \ + X(glFinish, void, (void)) \ + X(glGenBuffers, void, (GLsizei, GLuint *)) \ + X(glBindBuffer, void, (GLenum, GLuint)) \ + X(glBufferData, void, (GLenum, GLsizeiptr, const void *, GLenum)) \ + X(glBufferSubData, void, (GLenum, GLintptr, GLsizeiptr, const void *)) \ + X(glTexBuffer, void, (GLenum, GLenum, GLuint)) \ + X(glMapBufferRange, void *, (GLenum, GLintptr, GLsizeiptr, GLbitfield)) \ + X(glUnmapBuffer, GLboolean, (GLenum)) \ + X(glGenVertexArrays, void, (GLsizei, GLuint *)) \ + X(glBindVertexArray, void, (GLuint)) \ + X(glEnableVertexAttribArray, void, (GLuint)) \ + X(glVertexAttribPointer, void, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void *)) \ + X(glVertexAttribIPointer, void, (GLuint, GLint, GLenum, GLsizei, const void *)) \ + X(glCreateShader, GLuint, (GLenum)) \ + X(glShaderSource, void, (GLuint, GLsizei, const GLchar *const *, const GLint *)) \ + X(glCompileShader, void, (GLuint)) \ + X(glGetShaderiv, void, (GLuint, GLenum, GLint *)) \ + X(glGetShaderInfoLog, void, (GLuint, GLsizei, GLsizei *, GLchar *)) \ + X(glCreateProgram, GLuint, (void)) \ + X(glAttachShader, void, (GLuint, GLuint)) \ + X(glBindAttribLocation, void, (GLuint, GLuint, const GLchar *)) \ + X(glBindFragDataLocationIndexed, void, (GLuint, GLuint, GLuint, const GLchar *)) \ + X(glLinkProgram, void, (GLuint)) \ + X(glGetProgramiv, void, (GLuint, GLenum, GLint *)) \ + X(glGetProgramInfoLog, void, (GLuint, GLsizei, GLsizei *, GLchar *)) \ + X(glUseProgram, void, (GLuint)) \ + X(glGetUniformLocation, GLint, (GLuint, const GLchar *)) \ + X(glUniform1i, void, (GLint, GLint)) \ + X(glUniform4f, void, (GLint, GLfloat, GLfloat, GLfloat, GLfloat)) \ + X(glUniform2f, void, (GLint, GLfloat, GLfloat)) \ + X(glUniform2i, void, (GLint, GLint, GLint)) \ + X(glUniform4iv, void, (GLint, GLsizei, const GLint *)) \ + X(glUniform1iv, void, (GLint, GLsizei, const GLint *)) \ + X(glGenFramebuffers, void, (GLsizei, GLuint *)) \ + X(glDeleteFramebuffers, void, (GLsizei, const GLuint *)) \ + X(glBindFramebuffer, void, (GLenum, GLuint)) \ + X(glFramebufferTexture2D, void, (GLenum, GLenum, GLenum, GLuint, GLint)) \ + X(glCheckFramebufferStatus, GLenum, (GLenum)) \ + X(glBlitFramebuffer, void, (GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum)) \ + X(glProvokingVertex, void, (GLenum)) \ + X(glDrawBuffer, void, (GLenum)) \ + X(glReadBuffer, void, (GLenum)) + +#define GSGL_DECL(name, ret, args) typedef ret (GLAPIENTRY *PFN_##name) args; +GSGL_FUNCS(GSGL_DECL) +#undef GSGL_DECL + +/* the platform's loader: returns 0 for a missing entry point */ +void *plat_gl_proc(const char *name); +#endif diff --git a/games/rac1/host/src/gs/gs_gl_thread.h b/games/rac1/host/src/gs/gs_gl_thread.h new file mode 100644 index 0000000..a54e7bc --- /dev/null +++ b/games/rac1/host/src/gs/gs_gl_thread.h @@ -0,0 +1,647 @@ +/* generated by tools/gen_glthread.py from gs_gl_api.h: do not edit */ +/* included by gs_gl.c, after the GL thread helpers (glt_cmd, glt_call, glt_q, glt_pix_bytes) */ +enum { GLT_glGetError, GLT_glGetString, GLT_glGetIntegerv, GLT_glEnable, GLT_glDisable, GLT_glViewport, GLT_glScissor, GLT_glClearColor, GLT_glClear, GLT_glColorMask, GLT_glDepthMask, GLT_glDepthFunc, GLT_glBlendFuncSeparate, GLT_glBlendEquationSeparate, GLT_glBlendColor, GLT_glPixelStorei, GLT_glReadPixels, GLT_glGenTextures, GLT_glDeleteTextures, GLT_glBindTexture, GLT_glActiveTexture, GLT_glTexImage2D, GLT_glTexSubImage2D, GLT_glTexParameteri, GLT_glDrawArrays, GLT_glFlush, GLT_glFinish, GLT_glGenBuffers, GLT_glBindBuffer, GLT_glBufferData, GLT_glBufferSubData, GLT_glTexBuffer, GLT_glMapBufferRange, GLT_glUnmapBuffer, GLT_glGenVertexArrays, GLT_glBindVertexArray, GLT_glEnableVertexAttribArray, GLT_glVertexAttribPointer, GLT_glVertexAttribIPointer, GLT_glCreateShader, GLT_glShaderSource, GLT_glCompileShader, GLT_glGetShaderiv, GLT_glGetShaderInfoLog, GLT_glCreateProgram, GLT_glAttachShader, GLT_glBindAttribLocation, GLT_glBindFragDataLocationIndexed, GLT_glLinkProgram, GLT_glGetProgramiv, GLT_glGetProgramInfoLog, GLT_glUseProgram, GLT_glGetUniformLocation, GLT_glUniform1i, GLT_glUniform4f, GLT_glUniform2f, GLT_glUniform2i, GLT_glUniform4iv, GLT_glUniform1iv, GLT_glGenFramebuffers, GLT_glDeleteFramebuffers, GLT_glBindFramebuffer, GLT_glFramebufferTexture2D, GLT_glCheckFramebufferStatus, GLT_glBlitFramebuffer, GLT_glProvokingVertex, GLT_glDrawBuffer, GLT_glReadBuffer, GLT_SWAP, GLT_VB, GLT_CALLBACK, GLT_N }; +#define GSGL_DEF(name, ret, args) PFN_##name r_##name; +GSGL_FUNCS(GSGL_DEF) +#undef GSGL_DEF +static GLenum p_glGetError(void) +{ + if (!glt_q) return r_glGetError(); + u64 a[0 + 1]; u64 r = 0; + glt_call(GLT_glGetError, a, 0, &r); + return (GLenum)r; +} +static const GLubyte * p_glGetString(GLenum a0) +{ + if (!glt_q) return r_glGetString(a0); + u64 a[1 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + glt_call(GLT_glGetString, a, 1, &r); + return (const GLubyte *)(uintptr_t)r; +} +static void p_glGetIntegerv(GLenum a0, GLint * a1) +{ + if (!glt_q) { r_glGetIntegerv(a0, a1); return; } + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGetIntegerv, a, 2, &r); +} +static void p_glEnable(GLenum a0) +{ + if (!glt_q) { r_glEnable(a0); return; } + u64 *c = glt_cmd(GLT_glEnable, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glDisable(GLenum a0) +{ + if (!glt_q) { r_glDisable(a0); return; } + u64 *c = glt_cmd(GLT_glDisable, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glViewport(GLint a0, GLint a1, GLsizei a2, GLsizei a3) +{ + if (!glt_q) { r_glViewport(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glViewport, 4, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); +} +static void p_glScissor(GLint a0, GLint a1, GLsizei a2, GLsizei a3) +{ + if (!glt_q) { r_glScissor(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glScissor, 4, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); +} +static void p_glClearColor(GLfloat a0, GLfloat a1, GLfloat a2, GLfloat a3) +{ + if (!glt_q) { r_glClearColor(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glClearColor, 4, 0); + c[0] = glt_fbits(a0); + c[1] = glt_fbits(a1); + c[2] = glt_fbits(a2); + c[3] = glt_fbits(a3); +} +static void p_glClear(GLbitfield a0) +{ + if (!glt_q) { r_glClear(a0); return; } + u64 *c = glt_cmd(GLT_glClear, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glColorMask(GLboolean a0, GLboolean a1, GLboolean a2, GLboolean a3) +{ + if (!glt_q) { r_glColorMask(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glColorMask, 4, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); +} +static void p_glDepthMask(GLboolean a0) +{ + if (!glt_q) { r_glDepthMask(a0); return; } + u64 *c = glt_cmd(GLT_glDepthMask, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glDepthFunc(GLenum a0) +{ + if (!glt_q) { r_glDepthFunc(a0); return; } + u64 *c = glt_cmd(GLT_glDepthFunc, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glBlendFuncSeparate(GLenum a0, GLenum a1, GLenum a2, GLenum a3) +{ + if (!glt_q) { r_glBlendFuncSeparate(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glBlendFuncSeparate, 4, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); +} +static void p_glBlendEquationSeparate(GLenum a0, GLenum a1) +{ + if (!glt_q) { r_glBlendEquationSeparate(a0, a1); return; } + u64 *c = glt_cmd(GLT_glBlendEquationSeparate, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glBlendColor(GLfloat a0, GLfloat a1, GLfloat a2, GLfloat a3) +{ + if (!glt_q) { r_glBlendColor(a0, a1, a2, a3); return; } + u64 *c = glt_cmd(GLT_glBlendColor, 4, 0); + c[0] = glt_fbits(a0); + c[1] = glt_fbits(a1); + c[2] = glt_fbits(a2); + c[3] = glt_fbits(a3); +} +static void p_glPixelStorei(GLenum a0, GLint a1) +{ + glt_track_store(a0, a1); /* the unpack layout the queued texel copies are sized by */ + if (!glt_q) { r_glPixelStorei(a0, a1); return; } + u64 *c = glt_cmd(GLT_glPixelStorei, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glReadPixels(GLint a0, GLint a1, GLsizei a2, GLsizei a3, GLenum a4, GLenum a5, void * a6) +{ + if (!glt_q) { r_glReadPixels(a0, a1, a2, a3, a4, a5, a6); return; } + u64 a[7 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(s64)(a2); + a[3] = (u64)(s64)(a3); + a[4] = (u64)(s64)(a4); + a[5] = (u64)(s64)(a5); + a[6] = (u64)(uintptr_t)(a6); + glt_call(GLT_glReadPixels, a, 7, &r); +} +static void p_glGenTextures(GLsizei a0, GLuint * a1) +{ + if (!glt_q) { r_glGenTextures(a0, a1); return; } + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGenTextures, a, 2, &r); +} +static void p_glDeleteTextures(GLsizei a0, const GLuint * a1) +{ + if (!glt_q) { r_glDeleteTextures(a0, a1); return; } + size_t nb = a1 ? (size_t)a0 * 4 : 0; + u64 *c = glt_cmd(GLT_glDeleteTextures, 2, nb); + c[0] = (u64)(s64)(a0); + c[1] = 0; if (nb) memcpy(c + 2, a1, nb); +} +static void p_glBindTexture(GLenum a0, GLuint a1) +{ + if (!glt_q) { r_glBindTexture(a0, a1); return; } + u64 *c = glt_cmd(GLT_glBindTexture, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glActiveTexture(GLenum a0) +{ + if (!glt_q) { r_glActiveTexture(a0); return; } + u64 *c = glt_cmd(GLT_glActiveTexture, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glTexImage2D(GLenum a0, GLint a1, GLint a2, GLsizei a3, GLsizei a4, GLint a5, GLenum a6, GLenum a7, const void * a8) +{ + if (!glt_q) { r_glTexImage2D(a0, a1, a2, a3, a4, a5, a6, a7, a8); return; } + size_t nb = a8 ? glt_pix_bytes(a3, a4, a6, a7) : 0; + u64 *c = glt_cmd(GLT_glTexImage2D, 9, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(s64)(a4); + c[5] = (u64)(s64)(a5); + c[6] = (u64)(s64)(a6); + c[7] = (u64)(s64)(a7); + c[8] = 0; if (nb) memcpy(c + 9, a8, nb); +} +static void p_glTexSubImage2D(GLenum a0, GLint a1, GLint a2, GLint a3, GLsizei a4, GLsizei a5, GLenum a6, GLenum a7, const void * a8) +{ + if (!glt_q) { r_glTexSubImage2D(a0, a1, a2, a3, a4, a5, a6, a7, a8); return; } + size_t nb = a8 ? glt_pix_bytes(a4, a5, a6, a7) : 0; + u64 *c = glt_cmd(GLT_glTexSubImage2D, 9, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(s64)(a4); + c[5] = (u64)(s64)(a5); + c[6] = (u64)(s64)(a6); + c[7] = (u64)(s64)(a7); + c[8] = 0; if (nb) memcpy(c + 9, a8, nb); +} +static void p_glTexParameteri(GLenum a0, GLenum a1, GLint a2) +{ + if (!glt_q) { r_glTexParameteri(a0, a1, a2); return; } + u64 *c = glt_cmd(GLT_glTexParameteri, 3, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); +} +static void p_glDrawArrays(GLenum a0, GLint a1, GLsizei a2) +{ + if (!glt_q) { r_glDrawArrays(a0, a1, a2); return; } + u64 *c = glt_cmd(GLT_glDrawArrays, 3, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); +} +static void p_glFlush(void) +{ + if (!glt_q) { r_glFlush(); return; } + u64 *c = glt_cmd(GLT_glFlush, 0, 0); +} +static void p_glFinish(void) +{ + if (!glt_q) { r_glFinish(); return; } + u64 *c = glt_cmd(GLT_glFinish, 0, 0); +} +static void p_glGenBuffers(GLsizei a0, GLuint * a1) +{ + if (!glt_q) { r_glGenBuffers(a0, a1); return; } + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGenBuffers, a, 2, &r); +} +static void p_glBindBuffer(GLenum a0, GLuint a1) +{ + if (!glt_q) { r_glBindBuffer(a0, a1); return; } + u64 *c = glt_cmd(GLT_glBindBuffer, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glBufferData(GLenum a0, GLsizeiptr a1, const void * a2, GLenum a3) +{ + if (!glt_q) { r_glBufferData(a0, a1, a2, a3); return; } + size_t nb = a2 ? (size_t)a1 : 0; + u64 *c = glt_cmd(GLT_glBufferData, 4, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = 0; if (nb) memcpy(c + 4, a2, nb); + c[3] = (u64)(s64)(a3); +} +static void p_glBufferSubData(GLenum a0, GLintptr a1, GLsizeiptr a2, const void * a3) +{ + if (!glt_q) { r_glBufferSubData(a0, a1, a2, a3); return; } + size_t nb = a3 ? (size_t)a2 : 0; + u64 *c = glt_cmd(GLT_glBufferSubData, 4, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = 0; if (nb) memcpy(c + 4, a3, nb); +} +static void p_glTexBuffer(GLenum a0, GLenum a1, GLuint a2) +{ + if (!glt_q) { r_glTexBuffer(a0, a1, a2); return; } + u64 *c = glt_cmd(GLT_glTexBuffer, 3, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); +} +static void * p_glMapBufferRange(GLenum a0, GLintptr a1, GLsizeiptr a2, GLbitfield a3) +{ + if (!glt_q) return r_glMapBufferRange(a0, a1, a2, a3); + u64 a[4 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(s64)(a2); + a[3] = (u64)(s64)(a3); + glt_call(GLT_glMapBufferRange, a, 4, &r); + return (void *)(uintptr_t)r; +} +static GLboolean p_glUnmapBuffer(GLenum a0) +{ + if (!glt_q) return r_glUnmapBuffer(a0); + u64 a[1 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + glt_call(GLT_glUnmapBuffer, a, 1, &r); + return (GLboolean)r; +} +static void p_glGenVertexArrays(GLsizei a0, GLuint * a1) +{ + if (!glt_q) { r_glGenVertexArrays(a0, a1); return; } + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGenVertexArrays, a, 2, &r); +} +static void p_glBindVertexArray(GLuint a0) +{ + if (!glt_q) { r_glBindVertexArray(a0); return; } + u64 *c = glt_cmd(GLT_glBindVertexArray, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glEnableVertexAttribArray(GLuint a0) +{ + if (!glt_q) { r_glEnableVertexAttribArray(a0); return; } + u64 *c = glt_cmd(GLT_glEnableVertexAttribArray, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glVertexAttribPointer(GLuint a0, GLint a1, GLenum a2, GLboolean a3, GLsizei a4, const void * a5) +{ + if (!glt_q) { r_glVertexAttribPointer(a0, a1, a2, a3, a4, a5); return; } + u64 *c = glt_cmd(GLT_glVertexAttribPointer, 6, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(s64)(a4); + c[5] = (u64)(uintptr_t)(a5); +} +static void p_glVertexAttribIPointer(GLuint a0, GLint a1, GLenum a2, GLsizei a3, const void * a4) +{ + if (!glt_q) { r_glVertexAttribIPointer(a0, a1, a2, a3, a4); return; } + u64 *c = glt_cmd(GLT_glVertexAttribIPointer, 5, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(uintptr_t)(a4); +} +static GLuint p_glCreateShader(GLenum a0) +{ + if (!glt_q) return r_glCreateShader(a0); + u64 a[1 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + glt_call(GLT_glCreateShader, a, 1, &r); + return (GLuint)r; +} +static void p_glShaderSource(GLuint a0, GLsizei a1, const GLchar *const * a2, const GLint * a3) +{ + if (!glt_q) { r_glShaderSource(a0, a1, a2, a3); return; } + u64 a[4 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + a[3] = (u64)(uintptr_t)(a3); + glt_call(GLT_glShaderSource, a, 4, &r); +} +static void p_glCompileShader(GLuint a0) +{ + if (!glt_q) { r_glCompileShader(a0); return; } + u64 *c = glt_cmd(GLT_glCompileShader, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glGetShaderiv(GLuint a0, GLenum a1, GLint * a2) +{ + if (!glt_q) { r_glGetShaderiv(a0, a1, a2); return; } + u64 a[3 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + glt_call(GLT_glGetShaderiv, a, 3, &r); +} +static void p_glGetShaderInfoLog(GLuint a0, GLsizei a1, GLsizei * a2, GLchar * a3) +{ + if (!glt_q) { r_glGetShaderInfoLog(a0, a1, a2, a3); return; } + u64 a[4 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + a[3] = (u64)(uintptr_t)(a3); + glt_call(GLT_glGetShaderInfoLog, a, 4, &r); +} +static GLuint p_glCreateProgram(void) +{ + if (!glt_q) return r_glCreateProgram(); + u64 a[0 + 1]; u64 r = 0; + glt_call(GLT_glCreateProgram, a, 0, &r); + return (GLuint)r; +} +static void p_glAttachShader(GLuint a0, GLuint a1) +{ + if (!glt_q) { r_glAttachShader(a0, a1); return; } + u64 *c = glt_cmd(GLT_glAttachShader, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glBindAttribLocation(GLuint a0, GLuint a1, const GLchar * a2) +{ + if (!glt_q) { r_glBindAttribLocation(a0, a1, a2); return; } + u64 a[3 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + glt_call(GLT_glBindAttribLocation, a, 3, &r); +} +static void p_glBindFragDataLocationIndexed(GLuint a0, GLuint a1, GLuint a2, const GLchar * a3) +{ + if (!glt_q) { r_glBindFragDataLocationIndexed(a0, a1, a2, a3); return; } + u64 a[4 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(s64)(a2); + a[3] = (u64)(uintptr_t)(a3); + glt_call(GLT_glBindFragDataLocationIndexed, a, 4, &r); +} +static void p_glLinkProgram(GLuint a0) +{ + if (!glt_q) { r_glLinkProgram(a0); return; } + u64 *c = glt_cmd(GLT_glLinkProgram, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glGetProgramiv(GLuint a0, GLenum a1, GLint * a2) +{ + if (!glt_q) { r_glGetProgramiv(a0, a1, a2); return; } + u64 a[3 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + glt_call(GLT_glGetProgramiv, a, 3, &r); +} +static void p_glGetProgramInfoLog(GLuint a0, GLsizei a1, GLsizei * a2, GLchar * a3) +{ + if (!glt_q) { r_glGetProgramInfoLog(a0, a1, a2, a3); return; } + u64 a[4 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(s64)(a1); + a[2] = (u64)(uintptr_t)(a2); + a[3] = (u64)(uintptr_t)(a3); + glt_call(GLT_glGetProgramInfoLog, a, 4, &r); +} +static void p_glUseProgram(GLuint a0) +{ + if (!glt_q) { r_glUseProgram(a0); return; } + u64 *c = glt_cmd(GLT_glUseProgram, 1, 0); + c[0] = (u64)(s64)(a0); +} +static GLint p_glGetUniformLocation(GLuint a0, const GLchar * a1) +{ + if (!glt_q) return r_glGetUniformLocation(a0, a1); + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGetUniformLocation, a, 2, &r); + return (GLint)r; +} +static void p_glUniform1i(GLint a0, GLint a1) +{ + if (!glt_q) { r_glUniform1i(a0, a1); return; } + u64 *c = glt_cmd(GLT_glUniform1i, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glUniform4f(GLint a0, GLfloat a1, GLfloat a2, GLfloat a3, GLfloat a4) +{ + if (!glt_q) { r_glUniform4f(a0, a1, a2, a3, a4); return; } + u64 *c = glt_cmd(GLT_glUniform4f, 5, 0); + c[0] = (u64)(s64)(a0); + c[1] = glt_fbits(a1); + c[2] = glt_fbits(a2); + c[3] = glt_fbits(a3); + c[4] = glt_fbits(a4); +} +static void p_glUniform2f(GLint a0, GLfloat a1, GLfloat a2) +{ + if (!glt_q) { r_glUniform2f(a0, a1, a2); return; } + u64 *c = glt_cmd(GLT_glUniform2f, 3, 0); + c[0] = (u64)(s64)(a0); + c[1] = glt_fbits(a1); + c[2] = glt_fbits(a2); +} +static void p_glUniform2i(GLint a0, GLint a1, GLint a2) +{ + if (!glt_q) { r_glUniform2i(a0, a1, a2); return; } + u64 *c = glt_cmd(GLT_glUniform2i, 3, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); +} +static void p_glUniform4iv(GLint a0, GLsizei a1, const GLint * a2) +{ + if (!glt_q) { r_glUniform4iv(a0, a1, a2); return; } + size_t nb = a2 ? (size_t)a1 * 16 : 0; + u64 *c = glt_cmd(GLT_glUniform4iv, 3, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = 0; if (nb) memcpy(c + 3, a2, nb); +} +static void p_glUniform1iv(GLint a0, GLsizei a1, const GLint * a2) +{ + if (!glt_q) { r_glUniform1iv(a0, a1, a2); return; } + size_t nb = a2 ? (size_t)a1 * 4 : 0; + u64 *c = glt_cmd(GLT_glUniform1iv, 3, nb); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = 0; if (nb) memcpy(c + 3, a2, nb); +} +static void p_glGenFramebuffers(GLsizei a0, GLuint * a1) +{ + if (!glt_q) { r_glGenFramebuffers(a0, a1); return; } + u64 a[2 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + a[1] = (u64)(uintptr_t)(a1); + glt_call(GLT_glGenFramebuffers, a, 2, &r); +} +static void p_glDeleteFramebuffers(GLsizei a0, const GLuint * a1) +{ + if (!glt_q) { r_glDeleteFramebuffers(a0, a1); return; } + size_t nb = a1 ? (size_t)a0 * 4 : 0; + u64 *c = glt_cmd(GLT_glDeleteFramebuffers, 2, nb); + c[0] = (u64)(s64)(a0); + c[1] = 0; if (nb) memcpy(c + 2, a1, nb); +} +static void p_glBindFramebuffer(GLenum a0, GLuint a1) +{ + if (!glt_q) { r_glBindFramebuffer(a0, a1); return; } + u64 *c = glt_cmd(GLT_glBindFramebuffer, 2, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); +} +static void p_glFramebufferTexture2D(GLenum a0, GLenum a1, GLenum a2, GLuint a3, GLint a4) +{ + if (!glt_q) { r_glFramebufferTexture2D(a0, a1, a2, a3, a4); return; } + u64 *c = glt_cmd(GLT_glFramebufferTexture2D, 5, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(s64)(a4); +} +static GLenum p_glCheckFramebufferStatus(GLenum a0) +{ + if (!glt_q) return r_glCheckFramebufferStatus(a0); + u64 a[1 + 1]; u64 r = 0; + a[0] = (u64)(s64)(a0); + glt_call(GLT_glCheckFramebufferStatus, a, 1, &r); + return (GLenum)r; +} +static void p_glBlitFramebuffer(GLint a0, GLint a1, GLint a2, GLint a3, GLint a4, GLint a5, GLint a6, GLint a7, GLbitfield a8, GLenum a9) +{ + if (!glt_q) { r_glBlitFramebuffer(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9); return; } + u64 *c = glt_cmd(GLT_glBlitFramebuffer, 10, 0); + c[0] = (u64)(s64)(a0); + c[1] = (u64)(s64)(a1); + c[2] = (u64)(s64)(a2); + c[3] = (u64)(s64)(a3); + c[4] = (u64)(s64)(a4); + c[5] = (u64)(s64)(a5); + c[6] = (u64)(s64)(a6); + c[7] = (u64)(s64)(a7); + c[8] = (u64)(s64)(a8); + c[9] = (u64)(s64)(a9); +} +static void p_glProvokingVertex(GLenum a0) +{ + if (!glt_q) { r_glProvokingVertex(a0); return; } + u64 *c = glt_cmd(GLT_glProvokingVertex, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glDrawBuffer(GLenum a0) +{ + if (!glt_q) { r_glDrawBuffer(a0); return; } + u64 *c = glt_cmd(GLT_glDrawBuffer, 1, 0); + c[0] = (u64)(s64)(a0); +} +static void p_glReadBuffer(GLenum a0) +{ + if (!glt_q) { r_glReadBuffer(a0); return; } + u64 *c = glt_cmd(GLT_glReadBuffer, 1, 0); + c[0] = (u64)(s64)(a0); +} +static const unsigned char glt_nargs[GLT_N] = { 0, 1, 2, 1, 1, 4, 4, 4, 1, 4, 1, 1, 4, 2, 4, 2, 7, 2, 2, 2, 1, 9, 9, 3, 3, 0, 0, 2, 2, 4, 4, 3, 4, 1, 2, 1, 1, 6, 5, 1, 4, 1, 3, 4, 0, 2, 3, 4, 1, 3, 4, 1, 2, 2, 5, 3, 3, 3, 3, 2, 2, 2, 5, 1, 10, 1, 1, 1, 0, 1, 0 }; /* ..., SWAP, VB, CALLBACK */ +/* the GL thread: one recorded call (c: its arguments, then its data; rp: where a result goes) */ +static void glt_exec(u32 id, const u64 *c, size_t blob_n, u64 *rp) +{ + (void)blob_n; (void)rp; + switch (id) { + case GLT_glGetError: *rp = (u64)r_glGetError(); break; + case GLT_glGetString: *rp = (u64)(uintptr_t)r_glGetString((GLenum)(c[0])); break; + case GLT_glGetIntegerv: r_glGetIntegerv((GLenum)(c[0]), (GLint *)(uintptr_t)(c[1])); break; + case GLT_glEnable: r_glEnable((GLenum)(c[0])); break; + case GLT_glDisable: r_glDisable((GLenum)(c[0])); break; + case GLT_glViewport: r_glViewport((GLint)(c[0]), (GLint)(c[1]), (GLsizei)(c[2]), (GLsizei)(c[3])); break; + case GLT_glScissor: r_glScissor((GLint)(c[0]), (GLint)(c[1]), (GLsizei)(c[2]), (GLsizei)(c[3])); break; + case GLT_glClearColor: r_glClearColor(glt_bitsf(c[0]), glt_bitsf(c[1]), glt_bitsf(c[2]), glt_bitsf(c[3])); break; + case GLT_glClear: r_glClear((GLbitfield)(c[0])); break; + case GLT_glColorMask: r_glColorMask((GLboolean)(c[0]), (GLboolean)(c[1]), (GLboolean)(c[2]), (GLboolean)(c[3])); break; + case GLT_glDepthMask: r_glDepthMask((GLboolean)(c[0])); break; + case GLT_glDepthFunc: r_glDepthFunc((GLenum)(c[0])); break; + case GLT_glBlendFuncSeparate: r_glBlendFuncSeparate((GLenum)(c[0]), (GLenum)(c[1]), (GLenum)(c[2]), (GLenum)(c[3])); break; + case GLT_glBlendEquationSeparate: r_glBlendEquationSeparate((GLenum)(c[0]), (GLenum)(c[1])); break; + case GLT_glBlendColor: r_glBlendColor(glt_bitsf(c[0]), glt_bitsf(c[1]), glt_bitsf(c[2]), glt_bitsf(c[3])); break; + case GLT_glPixelStorei: r_glPixelStorei((GLenum)(c[0]), (GLint)(c[1])); break; + case GLT_glReadPixels: r_glReadPixels((GLint)(c[0]), (GLint)(c[1]), (GLsizei)(c[2]), (GLsizei)(c[3]), (GLenum)(c[4]), (GLenum)(c[5]), (void *)(uintptr_t)(c[6])); break; + case GLT_glGenTextures: r_glGenTextures((GLsizei)(c[0]), (GLuint *)(uintptr_t)(c[1])); break; + case GLT_glDeleteTextures: r_glDeleteTextures((GLsizei)(c[0]), blob_n ? (const GLuint *)(const void *)(c + 2) : (const GLuint *)0); break; + case GLT_glBindTexture: r_glBindTexture((GLenum)(c[0]), (GLuint)(c[1])); break; + case GLT_glActiveTexture: r_glActiveTexture((GLenum)(c[0])); break; + case GLT_glTexImage2D: r_glTexImage2D((GLenum)(c[0]), (GLint)(c[1]), (GLint)(c[2]), (GLsizei)(c[3]), (GLsizei)(c[4]), (GLint)(c[5]), (GLenum)(c[6]), (GLenum)(c[7]), blob_n ? (const void *)(const void *)(c + 9) : (const void *)0); break; + case GLT_glTexSubImage2D: r_glTexSubImage2D((GLenum)(c[0]), (GLint)(c[1]), (GLint)(c[2]), (GLint)(c[3]), (GLsizei)(c[4]), (GLsizei)(c[5]), (GLenum)(c[6]), (GLenum)(c[7]), blob_n ? (const void *)(const void *)(c + 9) : (const void *)0); break; + case GLT_glTexParameteri: r_glTexParameteri((GLenum)(c[0]), (GLenum)(c[1]), (GLint)(c[2])); break; + case GLT_glDrawArrays: r_glDrawArrays((GLenum)(c[0]), (GLint)(c[1]), (GLsizei)(c[2])); break; + case GLT_glFlush: r_glFlush(); break; + case GLT_glFinish: r_glFinish(); break; + case GLT_glGenBuffers: r_glGenBuffers((GLsizei)(c[0]), (GLuint *)(uintptr_t)(c[1])); break; + case GLT_glBindBuffer: r_glBindBuffer((GLenum)(c[0]), (GLuint)(c[1])); break; + case GLT_glBufferData: r_glBufferData((GLenum)(c[0]), (GLsizeiptr)(c[1]), blob_n ? (const void *)(const void *)(c + 4) : (const void *)0, (GLenum)(c[3])); break; + case GLT_glBufferSubData: r_glBufferSubData((GLenum)(c[0]), (GLintptr)(c[1]), (GLsizeiptr)(c[2]), blob_n ? (const void *)(const void *)(c + 4) : (const void *)0); break; + case GLT_glTexBuffer: r_glTexBuffer((GLenum)(c[0]), (GLenum)(c[1]), (GLuint)(c[2])); break; + case GLT_glMapBufferRange: *rp = (u64)(uintptr_t)r_glMapBufferRange((GLenum)(c[0]), (GLintptr)(c[1]), (GLsizeiptr)(c[2]), (GLbitfield)(c[3])); break; + case GLT_glUnmapBuffer: *rp = (u64)r_glUnmapBuffer((GLenum)(c[0])); break; + case GLT_glGenVertexArrays: r_glGenVertexArrays((GLsizei)(c[0]), (GLuint *)(uintptr_t)(c[1])); break; + case GLT_glBindVertexArray: r_glBindVertexArray((GLuint)(c[0])); break; + case GLT_glEnableVertexAttribArray: r_glEnableVertexAttribArray((GLuint)(c[0])); break; + case GLT_glVertexAttribPointer: r_glVertexAttribPointer((GLuint)(c[0]), (GLint)(c[1]), (GLenum)(c[2]), (GLboolean)(c[3]), (GLsizei)(c[4]), (const void *)(uintptr_t)(c[5])); break; + case GLT_glVertexAttribIPointer: r_glVertexAttribIPointer((GLuint)(c[0]), (GLint)(c[1]), (GLenum)(c[2]), (GLsizei)(c[3]), (const void *)(uintptr_t)(c[4])); break; + case GLT_glCreateShader: *rp = (u64)r_glCreateShader((GLenum)(c[0])); break; + case GLT_glShaderSource: r_glShaderSource((GLuint)(c[0]), (GLsizei)(c[1]), (const GLchar *const *)(uintptr_t)(c[2]), (const GLint *)(uintptr_t)(c[3])); break; + case GLT_glCompileShader: r_glCompileShader((GLuint)(c[0])); break; + case GLT_glGetShaderiv: r_glGetShaderiv((GLuint)(c[0]), (GLenum)(c[1]), (GLint *)(uintptr_t)(c[2])); break; + case GLT_glGetShaderInfoLog: r_glGetShaderInfoLog((GLuint)(c[0]), (GLsizei)(c[1]), (GLsizei *)(uintptr_t)(c[2]), (GLchar *)(uintptr_t)(c[3])); break; + case GLT_glCreateProgram: *rp = (u64)r_glCreateProgram(); break; + case GLT_glAttachShader: r_glAttachShader((GLuint)(c[0]), (GLuint)(c[1])); break; + case GLT_glBindAttribLocation: r_glBindAttribLocation((GLuint)(c[0]), (GLuint)(c[1]), (const GLchar *)(uintptr_t)(c[2])); break; + case GLT_glBindFragDataLocationIndexed: r_glBindFragDataLocationIndexed((GLuint)(c[0]), (GLuint)(c[1]), (GLuint)(c[2]), (const GLchar *)(uintptr_t)(c[3])); break; + case GLT_glLinkProgram: r_glLinkProgram((GLuint)(c[0])); break; + case GLT_glGetProgramiv: r_glGetProgramiv((GLuint)(c[0]), (GLenum)(c[1]), (GLint *)(uintptr_t)(c[2])); break; + case GLT_glGetProgramInfoLog: r_glGetProgramInfoLog((GLuint)(c[0]), (GLsizei)(c[1]), (GLsizei *)(uintptr_t)(c[2]), (GLchar *)(uintptr_t)(c[3])); break; + case GLT_glUseProgram: r_glUseProgram((GLuint)(c[0])); break; + case GLT_glGetUniformLocation: *rp = (u64)r_glGetUniformLocation((GLuint)(c[0]), (const GLchar *)(uintptr_t)(c[1])); break; + case GLT_glUniform1i: r_glUniform1i((GLint)(c[0]), (GLint)(c[1])); break; + case GLT_glUniform4f: r_glUniform4f((GLint)(c[0]), glt_bitsf(c[1]), glt_bitsf(c[2]), glt_bitsf(c[3]), glt_bitsf(c[4])); break; + case GLT_glUniform2f: r_glUniform2f((GLint)(c[0]), glt_bitsf(c[1]), glt_bitsf(c[2])); break; + case GLT_glUniform2i: r_glUniform2i((GLint)(c[0]), (GLint)(c[1]), (GLint)(c[2])); break; + case GLT_glUniform4iv: r_glUniform4iv((GLint)(c[0]), (GLsizei)(c[1]), blob_n ? (const GLint *)(const void *)(c + 3) : (const GLint *)0); break; + case GLT_glUniform1iv: r_glUniform1iv((GLint)(c[0]), (GLsizei)(c[1]), blob_n ? (const GLint *)(const void *)(c + 3) : (const GLint *)0); break; + case GLT_glGenFramebuffers: r_glGenFramebuffers((GLsizei)(c[0]), (GLuint *)(uintptr_t)(c[1])); break; + case GLT_glDeleteFramebuffers: r_glDeleteFramebuffers((GLsizei)(c[0]), blob_n ? (const GLuint *)(const void *)(c + 2) : (const GLuint *)0); break; + case GLT_glBindFramebuffer: r_glBindFramebuffer((GLenum)(c[0]), (GLuint)(c[1])); break; + case GLT_glFramebufferTexture2D: r_glFramebufferTexture2D((GLenum)(c[0]), (GLenum)(c[1]), (GLenum)(c[2]), (GLuint)(c[3]), (GLint)(c[4])); break; + case GLT_glCheckFramebufferStatus: *rp = (u64)r_glCheckFramebufferStatus((GLenum)(c[0])); break; + case GLT_glBlitFramebuffer: r_glBlitFramebuffer((GLint)(c[0]), (GLint)(c[1]), (GLint)(c[2]), (GLint)(c[3]), (GLint)(c[4]), (GLint)(c[5]), (GLint)(c[6]), (GLint)(c[7]), (GLbitfield)(c[8]), (GLenum)(c[9])); break; + case GLT_glProvokingVertex: r_glProvokingVertex((GLenum)(c[0])); break; + case GLT_glDrawBuffer: r_glDrawBuffer((GLenum)(c[0])); break; + case GLT_glReadBuffer: r_glReadBuffer((GLenum)(c[0])); break; + default: glt_exec_extra(id, c, blob_n); break; + } +} diff --git a/games/rac1/host/src/gs/gs_swizzle.h b/games/rac1/host/src/gs/gs_swizzle.h new file mode 100644 index 0000000..722d8c0 --- /dev/null +++ b/games/rac1/host/src/gs/gs_swizzle.h @@ -0,0 +1,60 @@ +/* gs_swizzle.h: where a pixel lives in GS local memory (4 MB) for each pixel + * storage format. Memory is pages of 8 KB (32 blocks of 256 bytes); inside a + * block, pixels are arranged in 4 columns. The layouts below are the ones the + * GS hardware uses (GS User's Manual, section 8). */ +#ifndef GS_SWIZZLE_H +#define GS_SWIZZLE_H +#include + +static const uint8_t gs_block32[4][8] = { + {0, 1, 4, 5, 16, 17, 20, 21}, {2, 3, 6, 7, 18, 19, 22, 23}, + {8, 9, 12, 13, 24, 25, 28, 29}, {10, 11, 14, 15, 26, 27, 30, 31}}; +static const uint8_t gs_block16[8][4] = { + {0, 2, 8, 10}, {1, 3, 9, 11}, {4, 6, 12, 14}, {5, 7, 13, 15}, + {16, 18, 24, 26}, {17, 19, 25, 27}, {20, 22, 28, 30}, {21, 23, 29, 31}}; +static const uint8_t gs_block16s[8][4] = { + {0, 2, 16, 18}, {1, 3, 17, 19}, {8, 10, 24, 26}, {9, 11, 25, 27}, + {4, 6, 20, 22}, {5, 7, 21, 23}, {12, 14, 28, 30}, {13, 15, 29, 31}}; +/* 8-bit uses the 32-bit block order, 4-bit the 16-bit one */ +#define gs_block8 gs_block32 +#define gs_block4 gs_block16 + +static uint8_t gs_col32[8][8]; /* word in block */ +static uint8_t gs_col16[8][16]; /* halfword in block */ +static uint8_t gs_col8[16][16]; /* byte in block */ +static uint16_t gs_col4[16][32]; /* nibble in block */ + +static void gs_swizzle_init(void) +{ + /* 32-bit: a column is 8x2 pixels; pairs of words alternate between rows */ + for (int y = 0; y < 8; y++) + for (int x = 0; x < 8; x++) + gs_col32[y][x] = (uint8_t)((y >> 1) * 16 + (x >> 1) * 4 + (y & 1) * 2 + (x & 1)); + /* 16-bit: two 16-bit pixels share a 32-bit word; x bit 3 picks the half */ + for (int y = 0; y < 8; y++) + for (int x = 0; x < 16; x++) + gs_col16[y][x] = (uint8_t)(gs_col32[y][x & 7] * 2 + ((x >> 3) & 1)); + /* 8-bit and 4-bit: each column (4 rows) is laid out like a 32-bit column + * in which odd columns swap their halves */ + for (int y = 0; y < 16; y++) + for (int x = 0; x < 16; x++) { + int col = y >> 2, r = y & 3; + int half = ((r >> 1) ^ (col & 1)) & 1; /* which half of the column's words */ + int grp = x >> 3, xi = x & 7; + int base[8] = {0, 4, 16, 20, 32, 36, 48, 52}; + int b = base[(xi + (half ? 4 : 0)) & 7]; + if (r >> 1) b += 1; /* rows 2 and 3 hold the odd bytes */ + gs_col8[y][x] = (uint8_t)(col * 64 + b + (r & 1) * 8 + grp * 2); + } + for (int y = 0; y < 16; y++) + for (int x = 0; x < 32; x++) { + int col = y >> 2, r = y & 3; + int half = ((r >> 1) ^ (col & 1)) & 1; + int grp = x >> 3, xi = x & 7; + int base[8] = {0, 8, 32, 40, 64, 72, 96, 104}; + int b = base[(xi + (half ? 4 : 0)) & 7]; + if (r >> 1) b += 1; + gs_col4[y][x] = (uint16_t)(col * 128 + b + (r & 1) * 16 + grp * 2); + } +} +#endif diff --git a/games/rac1/host/src/gs/gs_types.h b/games/rac1/host/src/gs/gs_types.h new file mode 100644 index 0000000..ef53a75 --- /dev/null +++ b/games/rac1/host/src/gs/gs_types.h @@ -0,0 +1,34 @@ +/* gs_types.h: the software GS's (gs.c) register context, vertex, primitive + * and pipeline-state types, shared with the GL renderer (gs_gl.c). */ +#ifndef GS_TYPES_H +#define GS_TYPES_H +#include "uya_host.h" + +typedef struct { + u64 tex0, tex1, clamp, xyoffset, scissor, alpha, test, fba, frame, zbuf, miptbp1, miptbp2; +} gs_ctx_t; + +typedef struct { float x, y; u32 z; u8 r, g, b, a; float s, t, q, u, v; u8 f; } vtx_t; + +/* a texture level as the drawing sees it: w, h: decoded; lw, lh: the level's + * size (REPEAT/CLAMP); tci, ser: the texture cache entry and its build (GL) */ +typedef struct { const u32 *px; int w, h, lw, lh; int tci; u32 ser; } tlevel_t; +typedef struct { + u8 tme, fge, abe, fst, iip, ate, atst, afail, zte, ztst, zmsk; + u8 ba, bb, bc, bd, tfx, tcc, fba, clampc, lcm, mxl, mmag, mmin, l, wms, wmt, dthe_; + u8 date, datm; /* destination alpha test: only pixels whose alpha MSB is DATM are drawn */ + u8 aref, fix, fogr, fogg, fogb; + u8 defer; /* GL: levels not decoded yet (their memory is a GPU target's) */ + u16 rt; /* GL: the texture is a GPU target (index + 1) */ + u32 fbp, fbw, fpsm, fbmsk, zbp, zpsm; + int sx0, sx1, sy0, sy1; + int minu, maxu, minv, maxv; + float tw, th, k; + tlevel_t lv[7]; + u64 tex0, tex1, miptbp1, miptbp2; /* for the REGION modes' reads outside a level */ + u64 clamp, texa; /* GL: for decoding a deferred level */ +} pcmd_t; +typedef struct { u32 st; u8 type; vtx_t v[3]; } pprim_t; /* a primitive: its state (index) and vertices */ + +#define TC_N 4096 /* texture cache entries */ +#endif diff --git a/games/rac1/host/src/main.c b/games/rac1/host/src/main.c new file mode 100644 index 0000000..eb562a9 --- /dev/null +++ b/games/rac1/host/src/main.c @@ -0,0 +1,93 @@ +/* RAC1 Wrapper: runs the decompiled Ratchet & Clank (PAL, SCES_509.16) natively. + * usage: rac1_wrapper [SCES_509.16 or -] [disc.iso] (the executable is read from the disc by default) + * Both come from the user's own disc; nothing from them is in this project. */ +#include "uya_runtime.h" +#include +#include +#include + +/* The disc image: the second argument, RAC1_ISO, or `iso=PATH` in + * rac1_wrapper.cfg next to the program. */ +static const char *cfg_value(const char *key) +{ + static char line[4][1024]; + static int k; + char p[1100]; + snprintf(p, sizeof p, "%s/rac1_wrapper.cfg", plat_exe_dir()); + FILE *f = fopen(p, "r"); + if (!f) return 0; + char *b = line[k++ & 3]; + size_t kl = strlen(key); + while (fgets(b, sizeof line[0], f)) { + b[strcspn(b, "\r\n")] = 0; + if (!strncmp(b, key, kl) && b[kl] == '=') { fclose(f); return b + kl + 1; } + } + fclose(f); + return 0; +} + +static const char *find_iso(int argc, char **argv) +{ + if (argc > 2) return argv[2]; + if (getenv("RAC1_ISO")) return getenv("RAC1_ISO"); + const char *v = cfg_value("iso"); + if (v) return v; + uya_log("no disc image: put `iso=C:\\path\\to\\Ratchet & Clank (Europe).iso` in %s/rac1_wrapper.cfg", plat_exe_dir()); + return 0; +} + +/* SCES_509.16: read from the disc image itself (`disc:`), unless the first + * argument or `elf=` in the cfg names a file. */ +static const char *find_elf(int argc, char **argv) +{ + if (argc > 1 && strcmp(argv[1], "-")) return argv[1]; + const char *v = cfg_value("elf"); + if (v) return v; + return "disc:SCES_509.16"; +} + +/* Sections of the executable: everything but code. .vutext is VU microcode + * (data the game uploads by DMA). */ +static const char *const exe_sections[] = { + ".vutext", "core.data", "core.rdata", "core.bss", "core.lit", + ".lit", ".bss", ".data", "lvl.vtbl", "lvl.camvtbl", "lvl.sndvtbl", 0 +}; + +void rac1_boot(void); +void rac1_run(void); + +int uya_main(int argc, char **argv) +{ + setvbuf(stdout, 0, _IONBF, 0); + atexit(plat_at_exit); + uya_log_open(); + { /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */ + extern const char *uya_renderer_cfg; + const char *r = cfg_value("renderer"); + uya_renderer_cfg = r ? strdup(r) : 0; + } + const char *elf = find_elf(argc, argv); + if (plat_map_fixed(PS2_RAM_LO, PS2_RAM_HI - PS2_RAM_LO, 1) || + plat_map_fixed(PS2_SPR, PS2_SPR_SIZE, 0) || + plat_map_fixed(PS2_EE_REGS, PS2_EE_REGS_SZ, 0) || + plat_map_fixed(PS2_VU_MEM, PS2_VU_MEM_SZ, 0) || + plat_map_fixed(PS2_GS_PRIV, PS2_GS_PRIV_SZ, 0)) { + uya_log("the PS2 address space could not be mapped"); + return 1; + } + plat_install_trap_handler(); + { + const char *iso = find_iso(argc, argv); + if (!iso || uya_disc_open(iso) != 0) { uya_log("the disc image could not be opened"); return 1; } + uya_log("disc image: %s", iso); + } + if (uya_load_elf_sections(elf, exe_sections) <= 0) return 1; + /* code: every byte traps until a trampoline covers it */ + uya_fill_code_traps(0x00112380, 0x0012F580); /* core.text */ + uya_fill_code_traps(0x001E9080, 0x0023E740); /* .text (the game program) */ + uya_install_trampolines(); + atexit(uya_report); + rac1_boot(); + rac1_run(); + return 0; +} diff --git a/games/rac1/host/src/main_linux.c b/games/rac1/host/src/main_linux.c new file mode 100644 index 0000000..0e171ff --- /dev/null +++ b/games/rac1/host/src/main_linux.c @@ -0,0 +1,4 @@ +#ifdef __linux__ +#include "uya_runtime.h" +int main(int argc, char **argv) { return uya_main(argc, argv); } +#endif diff --git a/games/rac1/host/src/movie/pl_mpeg2.h b/games/rac1/host/src/movie/pl_mpeg2.h new file mode 100644 index 0000000..7a6c292 --- /dev/null +++ b/games/rac1/host/src/movie/pl_mpeg2.h @@ -0,0 +1,4616 @@ +/* +UYA Wrapper note: this is pl_mpeg by Dominic Szablewski (MIT licence, notice +below), extended for the MPEG-2 subset the PS2 movies use: progressive frame +pictures with frame prediction (sequence/picture coding/quant matrix +extensions, separate horizontal/vertical f_codes, intra DC precision, +non-linear quantiser scale, intra VLC table B.15, MPEG-2 escape coding, +MPEG-2 dequantisation with mismatch control). Field pictures and field/dual +prime prediction are not supported. Changes are marked "MPEG-2". +*/ +/* +PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer +SPDX-License-Identifier: MIT + +Dominic Szablewski - https://phoboslab.org + + +-- Synopsis + +// Define `PL_MPEG_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define PL_MPEG_IMPLEMENTATION +#include "plmpeg.h" + +// This function gets called for each decoded video frame +void my_video_callback(plm_t *plm, plm_frame_t *frame, void *user) { + // Do something with frame->y.data, frame->cr.data, frame->cb.data +} + +// This function gets called for each decoded audio frame +void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) { + // Do something with samples->interleaved +} + +// Load a .mpg (MPEG Program Stream) file +plm_t *plm = plm_create_with_filename("some-file.mpg"); + +// Install the video & audio decode callbacks +plm_set_video_decode_callback(plm, my_video_callback, my_data); +plm_set_audio_decode_callback(plm, my_audio_callback, my_data); + + +// Decode +do { + plm_decode(plm, time_since_last_call); +} while (!plm_has_ended(plm)); + +// All done +plm_destroy(plm); + + + +-- Documentation + +This library provides several interfaces to load, demux and decode MPEG video +and audio data. A high-level API combines the demuxer, video & audio decoders +in an easy to use wrapper. + +Lower-level APIs for accessing the demuxer, video decoder and audio decoder, +as well as providing different data sources are also available. + +Interfaces are written in an object oriented style, meaning you create object +instances via various different constructor functions (plm_*create()), +do some work on them and later dispose them via plm_*destroy(). + +plm_* ......... the high-level interface, combining demuxer and decoders +plm_buffer_* .. the data source used by all interfaces +plm_demux_* ... the MPEG-PS demuxer +plm_video_* ... the MPEG1 Video ("mpeg1") decoder +plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder + + +With the high-level interface you have two options to decode video & audio: + + 1. Use plm_decode() and just hand over the delta time since the last call. + It will decode everything needed and call your callbacks (specified through + plm_set_{video|audio}_decode_callback()) any number of times. + + 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one + frame of video or audio data at a time. How you handle the synchronization + of both streams is up to you. + +If you only want to decode video *or* audio through these functions, you should +disable the other stream (plm_set_{video|audio}_enabled(FALSE)) + +Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in +separate buffers. You can either convert this to RGB on the CPU (slow) via the +plm_frame_to_rgb() function or do it on the GPU with the following matrix: + +mat4 bt601 = mat4( + 1.16438, 0.00000, 1.59603, -0.87079, + 1.16438, -0.39176, -0.81297, 0.52959, + 1.16438, 2.01723, 0.00000, -1.08139, + 0, 0, 0, 1 +); +gl_FragColor = vec4(y, cb, cr, 1.0) * bt601; + +Audio data is decoded into a struct with either one single float array with the +samples for the left and right channel interleaved, or if the +PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into +two separate float arrays - one for each channel. + + +Data can be supplied to the high level interface, the demuxer and the decoders +in three different ways: + + 1. Using plm_create_from_filename() or with a file handle with + plm_create_from_file(). + + 2. Using plm_create_with_memory() and supplying a pointer to memory that + contains the whole file. + + 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and + periodically writing to this buffer. + +When using your own plm_buffer_t instance, you can fill this buffer using +plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push +data when appropriate, or install a callback on the buffer with +plm_buffer_set_load_callback() that gets called whenever the buffer needs more +data. + +A buffer created with plm_buffer_create_with_capacity() is treated as a ring +buffer, meaning that data that has already been read, will be discarded. In +contrast, a buffer created with plm_buffer_create_for_appending() will keep all +data written to it in memory. This enables seeking in the already loaded data. + + +There should be no need to use the lower level plm_demux_*, plm_video_* and +plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file. +However, if you get raw mpeg1video data or raw mp2 audio data from a different +source, these functions can be used to decode the raw data directly. Similarly, +if you only want to analyze an MPEG-PS file or extract raw video or audio +packets from it, you can use the plm_demux_* functions. + + +This library uses malloc(), realloc() and free() to manage memory. Typically +all allocation happens up-front when creating the interface. However, the +default buffer size may be too small for certain inputs. In these cases plmpeg +will realloc() the buffer with a larger size whenever needed. You can configure +the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before* +including this library. + +You can also define PLM_MALLOC, PLM_REALLOC and PLM_FREE to provide your own +memory management functions. + + +See below for detailed the API documentation. + +*/ + + +#ifndef PL_MPEG_H +#define PL_MPEG_H + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +// ----------------------------------------------------------------------------- +// Public Data Types + + +// Object types for the various interfaces + +typedef struct plm_t plm_t; +typedef struct plm_buffer_t plm_buffer_t; +typedef struct plm_demux_t plm_demux_t; +typedef struct plm_video_t plm_video_t; +typedef struct plm_audio_t plm_audio_t; + + +// Demuxed MPEG PS packet +// The type maps directly to the various MPEG-PES start codes. PTS is the +// presentation time stamp of the packet in seconds. Note that not all packets +// have a PTS value, indicated by PLM_PACKET_INVALID_TS. + +#define PLM_PACKET_INVALID_TS -1 + +typedef struct { + int type; + double pts; + size_t length; + uint8_t *data; +} plm_packet_t; + + +// Decoded Video Plane +// The byte length of the data is width * height. Note that different planes +// have different sizes: the Luma plane (Y) is double the size of each of +// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length. +// Also note that the size of the plane does *not* denote the size of the +// displayed frame. The sizes of planes are always rounded up to the nearest +// macroblock (16px). + +typedef struct { + unsigned int width; + unsigned int height; + uint8_t *data; +} plm_plane_t; + + +// Decoded Video Frame +// width and height denote the desired display size of the frame. This may be +// different from the internal size of the 3 planes. + +typedef struct { + double time; + unsigned int width; + unsigned int height; + plm_plane_t y; + plm_plane_t cr; + plm_plane_t cb; +} plm_frame_t; + + +// Callback function type for decoded video frames used by the high-level +// plm_* interface + +typedef void(*plm_video_decode_callback) + (plm_t *self, plm_frame_t *frame, void *user); + + +// Decoded Audio Samples +// Samples are stored as normalized (-1, 1) float either interleaved, or if +// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays. +// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for +// convenience. + +#define PLM_AUDIO_SAMPLES_PER_FRAME 1152 + +typedef struct { + double time; + unsigned int count; + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float left[PLM_AUDIO_SAMPLES_PER_FRAME]; + float right[PLM_AUDIO_SAMPLES_PER_FRAME]; + #else + float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2]; + #endif +} plm_samples_t; + + +// Callback function type for decoded audio samples used by the high-level +// plm_* interface + +typedef void(*plm_audio_decode_callback) + (plm_t *self, plm_samples_t *samples, void *user); + + +// Callback function for plm_buffer when it needs more data + +typedef void(*plm_buffer_load_callback)(plm_buffer_t *self, void *user); + + +// Callback function for plm_buffer when it needs to seek + +typedef void(*plm_buffer_seek_callback)(plm_buffer_t *self, size_t offset, void *user); + + +// Callback function for plm_buffer when it needs to tell the position + +typedef size_t(*plm_buffer_tell_callback)(plm_buffer_t *self, void *user); + + +// ----------------------------------------------------------------------------- +// plm_* public API +// High-Level API for loading/demuxing/decoding MPEG-PS data + +#ifndef PLM_NO_STDIO + +// Create a plmpeg instance with a filename. Returns NULL if the file could not +// be opened. + +plm_t *plm_create_with_filename(const char *filename); + + +// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to +// let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_t *plm_create_with_file(FILE *fh, int close_when_done); + +#endif // PLM_NO_STDIO + + +// Create a plmpeg instance with a pointer to memory as source. This assumes the +// whole file is in memory. The memory is not copied. Pass TRUE to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to +// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when +// plm_destroy() is called. + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a plmpeg instance and free all data. + +void plm_destroy(plm_t *self); + + +// Get whether we have headers on all available streams and we can report the +// number of video/audio streams, video dimensions, framerate and audio +// samplerate. +// This returns FALSE if the file is not an MPEG-PS file or - when not using a +// file as source - when not enough data is available yet. + +int plm_has_headers(plm_t *self); + + +// Probe the MPEG-PS data to find the actual number of video and audio streams +// within the buffer. For certain files (e.g. VideoCD) this can be more accurate +// than just reading the number of streams from the headers. +// This should only be used when the underlying plm_buffer is seekable, i.e. for +// files, fixed memory buffers or _for_appending buffers. If used with dynamic +// memory buffers it will skip decoding the probesize! +// The necessary probesize is dependent on the files you expect to read. Usually +// a few hundred KB should be enough to find all streams. +// Use plm_get_num_{audio|video}_streams() afterwards to get the number of +// streams in the file. +// Returns TRUE if any streams were found within the probesize. + +int plm_probe(plm_t *self, size_t probesize); + + +// Get or set whether video decoding is enabled. Default TRUE. + +int plm_get_video_enabled(plm_t *self); +void plm_set_video_enabled(plm_t *self, int enabled); + + +// Get the number of video streams (0--1) reported in the system header. + +int plm_get_num_video_streams(plm_t *self); + + +// Get the display width/height of the video stream. + +int plm_get_width(plm_t *self); +int plm_get_height(plm_t *self); +double plm_get_pixel_aspect_ratio(plm_t *self); + + +// Get the framerate of the video stream in frames per second. + +double plm_get_framerate(plm_t *self); + + +// Get or set whether audio decoding is enabled. Default TRUE. + +int plm_get_audio_enabled(plm_t *self); +void plm_set_audio_enabled(plm_t *self, int enabled); + + +// Get the number of audio streams (0--4) reported in the system header. + +int plm_get_num_audio_streams(plm_t *self); + + +// Set the desired audio stream (0--3). Default 0. + +void plm_set_audio_stream(plm_t *self, int stream_index); + + +// Get the samplerate of the audio stream in samples per second. + +int plm_get_samplerate(plm_t *self); + + +// Get or set the audio lead time in seconds - the time in which audio samples +// are decoded in advance (or behind) the video decode time. Typically this +// should be set to the duration of the buffer of the audio API that you use +// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate) + +double plm_get_audio_lead_time(plm_t *self); +void plm_set_audio_lead_time(plm_t *self, double lead_time); + + +// Get the current internal time in seconds. + +double plm_get_time(plm_t *self); + + +// Get the video duration of the underlying source in seconds. + +double plm_get_duration(plm_t *self); + + +// Rewind all buffers back to the beginning. + +void plm_rewind(plm_t *self); + + +// Get or set looping. Default FALSE. + +int plm_get_loop(plm_t *self); +void plm_set_loop(plm_t *self, int loop); + + +// Get whether the file has ended. If looping is enabled, this will always +// return FALSE. + +int plm_has_ended(plm_t *self); + + +// Set the callback for decoded video frames used with plm_decode(). If no +// callback is set, video data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user); + + +// Set the callback for decoded audio samples used with plm_decode(). If no +// callback is set, audio data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user); + + +// Advance the internal timer by seconds and decode video/audio up to this time. +// This will call the video_decode_callback and audio_decode_callback any number +// of times. A frame-skip is not implemented, i.e. everything up to current time +// will be decoded. + +void plm_decode(plm_t *self, double seconds); + + +// Decode and return one video frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode video, you should disable audio via plm_set_audio_enabled(). +// The returned plm_frame_t is valid until the next call to plm_decode_video() +// or until plm_destroy() is called. + +plm_frame_t *plm_decode_video(plm_t *self); + + +// Decode and return one audio frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode audio, you should disable video via plm_set_video_enabled(). +// The returned plm_samples_t is valid until the next call to plm_decode_audio() +// or until plm_destroy() is called. + +plm_samples_t *plm_decode_audio(plm_t *self); + + +// Seek to the specified time, clamped between 0 -- duration. This can only be +// used when the underlying plm_buffer is seekable, i.e. for files, fixed +// memory buffers or _for_appending buffers. +// If seek_exact is TRUE this will seek to the exact time, otherwise it will +// seek to the last intra frame just before the desired time. Exact seeking can +// be slow, because all frames up to the seeked one have to be decoded on top of +// the previous intra frame. +// If seeking succeeds, this function will call the video_decode_callback +// exactly once with the target frame. If audio is enabled, it will also call +// the audio_decode_callback any number of times, until the audio_lead_time is +// satisfied. +// Returns TRUE if seeking succeeded or FALSE if no frame could be found. + +int plm_seek(plm_t *self, double time, int seek_exact); + + +// Similar to plm_seek(), but will not call the video_decode_callback, +// audio_decode_callback or make any attempts to sync audio. +// Returns the found frame or NULL if no frame could be found. + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact); + + + +// ----------------------------------------------------------------------------- +// plm_buffer public API +// Provides the data source for all other plm_* interfaces + + +// The default size for buffers created from files or by the high-level API + +#ifndef PLM_BUFFER_DEFAULT_SIZE +#define PLM_BUFFER_DEFAULT_SIZE (128 * 1024) +#endif + +#ifndef PLM_NO_STDIO + +// Create a buffer instance with a filename. Returns NULL if the file could not +// be opened. + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename); + + +// Create a buffer instance with a file handle. Pass TRUE to close_when_done +// to let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done); + +#endif // PLM_NO_STDIO + + +// Create a buffer instance with custom callbacks for loading, seeking and +// telling the position. This behaves like a file handle, but with user-defined +// callbacks, useful for file handles that don't use the standard FILE API. +// Setting the length and closing/freeing has to be done manually. + +plm_buffer_t *plm_buffer_create_with_callbacks( + plm_buffer_load_callback load_callback, + plm_buffer_seek_callback seek_callback, + plm_buffer_tell_callback tell_callback, + size_t length, + void *user +); + + +// Create a buffer instance with a pointer to memory as source. This assumes +// the whole file is in memory. The bytes are not copied. Pass 1 to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Data that has already been read, will be discarded. + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Decoded data will *not* be discarded. This can be used when +// loading a file over the network, without needing to throttle the download. +// It also allows for seeking in the already loaded data. + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity); + + +// Destroy a buffer instance and free all data + +void plm_buffer_destroy(plm_buffer_t *self); + + +// Copy data into the buffer. If the data to be written is larger than the +// available space, the buffer will realloc() with a larger capacity. +// Returns the number of bytes written. This will always be the same as the +// passed in length, except when the buffer was created _with_memory() for +// which _write() is forbidden. + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length); + + +// Mark the current byte length as the end of this buffer and signal that no +// more data is expected to be written to it. This function should be called +// just after the last plm_buffer_write(). +// For _with_capacity buffers, this is cleared on a plm_buffer_rewind(). + +void plm_buffer_signal_end(plm_buffer_t *self); + + +// Set a callback that is called whenever the buffer needs more data + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user); + + +// Rewind the buffer back to the beginning. When loading from a file handle, +// this also seeks to the beginning of the file. + +void plm_buffer_rewind(plm_buffer_t *self); + + +// Get the total size. For files, this returns the file size. For all other +// types it returns the number of bytes currently in the buffer. + +size_t plm_buffer_get_size(plm_buffer_t *self); + + +// Get the number of remaining (yet unread) bytes in the buffer. This can be +// useful to throttle writing. + +size_t plm_buffer_get_remaining(plm_buffer_t *self); + + +// Get whether the read position of the buffer is at the end and no more data +// is expected. + +int plm_buffer_has_ended(plm_buffer_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_demux public API +// Demux an MPEG Program Stream (PS) data into separate packages + + +// Various Packet Types + +static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD; +static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0; +static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1; +static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2; +static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC3; +static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0; + + +// Create a demuxer with a plm_buffer as source. This will also attempt to read +// the pack and system headers from the buffer. + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a demuxer and free all data. + +void plm_demux_destroy(plm_demux_t *self); + + +// Returns TRUE/FALSE whether pack and system headers have been found. This will +// attempt to read the headers if non are present yet. + +int plm_demux_has_headers(plm_demux_t *self); + + +// Probe the file for the actual number of video/audio streams. See +// plm_probe() for the details. + +int plm_demux_probe(plm_demux_t *self, size_t probesize); + + +// Returns the number of video streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_video_streams(plm_demux_t *self); + + +// Returns the number of audio streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_audio_streams(plm_demux_t *self); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_demux_rewind(plm_demux_t *self); + + +// Get whether the file has ended. This will be cleared on seeking or rewind. + +int plm_demux_has_ended(plm_demux_t *self); + + +// Seek to a packet of the specified type with a PTS just before specified time. +// If force_intra is TRUE, only packets containing an intra frame will be +// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1. +// Note that the specified time is considered 0-based, regardless of the first +// PTS in the data source. + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double time, int type, int force_intra); + + +// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS +// if not packet of this packet type can be found. + +double plm_demux_get_start_time(plm_demux_t *self, int type); + + +// Get the duration for the specified packet type - i.e. the span between the +// the first PTS and the last PTS in the data source. This only makes sense when +// the underlying data source is a file or fixed memory. + +double plm_demux_get_duration(plm_demux_t *self, int type); + + +// Decode and return the next packet. The returned packet_t is valid until +// the next call to plm_demux_decode() or until the demuxer is destroyed. + +plm_packet_t *plm_demux_decode(plm_demux_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_video public API +// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames + + +// Create a video decoder with a plm_buffer as source. + +plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a video decoder and free all data. + +void plm_video_destroy(plm_video_t *self); + + +// Get whether a sequence header was found and we can accurately report on +// dimensions and framerate. + +int plm_video_has_header(plm_video_t *self); + + +// Get the framerate in frames per second. + +double plm_video_get_framerate(plm_video_t *self); +double plm_video_get_pixel_aspect_ratio(plm_video_t *self); + + +// Get the display width/height. + +int plm_video_get_width(plm_video_t *self); +int plm_video_get_height(plm_video_t *self); + + +// Set "no delay" mode. When enabled, the decoder assumes that the video does +// *not* contain any B-Frames. This is useful for reducing lag when streaming. +// The default is FALSE. + +void plm_video_set_no_delay(plm_video_t *self, int no_delay); + + +// Get the current internal time in seconds. + +double plm_video_get_time(plm_video_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_video_set_time(plm_video_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_video_rewind(plm_video_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_video_has_ended(plm_video_t *self); + + +// Decode and return one frame of video and advance the internal time by +// 1/framerate seconds. The returned frame_t is valid until the next call of +// plm_video_decode() or until the video decoder is destroyed. + +plm_frame_t *plm_video_decode(plm_video_t *self); + + +// Convert the YCrCb data of a frame into interleaved R G B data. The stride +// specifies the width in bytes of the destination buffer. I.e. the number of +// bytes from one line to the next. The stride must be at least +// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a +// size of at least (stride * frame->height). +// Note that the alpha component of the dest buffer is always left untouched. + +void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride); + + +// ----------------------------------------------------------------------------- +// plm_audio public API +// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples + + +// Create an audio decoder with a plm_buffer as source. + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy an audio decoder and free all data. + +void plm_audio_destroy(plm_audio_t *self); + + +// Get whether a frame header was found and we can accurately report on +// samplerate. + +int plm_audio_has_header(plm_audio_t *self); + + +// Get the samplerate in samples per second. + +int plm_audio_get_samplerate(plm_audio_t *self); + + +// Get the current internal time in seconds. + +double plm_audio_get_time(plm_audio_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_audio_set_time(plm_audio_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_audio_rewind(plm_audio_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_audio_has_ended(plm_audio_t *self); + + +// Decode and return one "frame" of audio and advance the internal time by +// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t +// is valid until the next call of plm_audio_decode() or until the audio +// decoder is destroyed. + +plm_samples_t *plm_audio_decode(plm_audio_t *self); + + + +#ifdef __cplusplus +} +#endif + +#endif // PL_MPEG_H + + + + + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// IMPLEMENTATION + +#ifdef PL_MPEG_IMPLEMENTATION + +#include +#include +#ifndef PLM_NO_STDIO +#include +#endif + +#ifndef TRUE +#define TRUE 1 +#define FALSE 0 +#endif + +#ifndef PLM_MALLOC + #define PLM_MALLOC(sz) malloc(sz) + #define PLM_FREE(p) free(p) + #define PLM_REALLOC(p, sz) realloc(p, sz) +#endif + +#define PLM_UNUSED(expr) (void)(expr) +#ifdef _MSC_VER + #pragma warning(disable:4996) +#endif + +// ----------------------------------------------------------------------------- +// plm (high-level interface) implementation + +struct plm_t { + plm_demux_t *demux; + double time; + int has_ended; + int loop; + int has_decoders; + + int video_enabled; + int video_packet_type; + plm_buffer_t *video_buffer; + plm_video_t *video_decoder; + + int audio_enabled; + int audio_stream_index; + int audio_packet_type; + double audio_lead_time; + plm_buffer_t *audio_buffer; + plm_audio_t *audio_decoder; + + plm_video_decode_callback video_decode_callback; + void *video_decode_callback_user_data; + + plm_audio_decode_callback audio_decode_callback; + void *audio_decode_callback_user_data; +}; + +int plm_init_decoders(plm_t *self); +void plm_handle_end(plm_t *self); +void plm_read_video_packet(plm_buffer_t *buffer, void *user); +void plm_read_audio_packet(plm_buffer_t *buffer, void *user); +void plm_read_packets(plm_t *self, int requested_type); + +#ifndef PLM_NO_STDIO + +plm_t *plm_create_with_filename(const char *filename) { + plm_buffer_t *buffer = plm_buffer_create_with_filename(filename); + if (!buffer) { + return NULL; + } + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +#endif // PLM_NO_STDIO + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_memory(bytes, length, free_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_t *self = (plm_t *)PLM_MALLOC(sizeof(plm_t)); + memset(self, 0, sizeof(plm_t)); + + self->demux = plm_demux_create(buffer, destroy_when_done); + self->video_enabled = TRUE; + self->audio_enabled = TRUE; + plm_init_decoders(self); + + return self; +} + +int plm_init_decoders(plm_t *self) { + if (self->has_decoders) { + return TRUE; + } + + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (plm_demux_get_num_video_streams(self->demux) > 0) { + if (self->video_enabled) { + self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1; + } + if (!self->video_decoder) { + self->video_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet, self); + self->video_decoder = plm_video_create_with_buffer(self->video_buffer, TRUE); + } + } + + if (plm_demux_get_num_audio_streams(self->demux) > 0) { + if (self->audio_enabled) { + self->audio_packet_type = PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index; + } + if (!self->audio_decoder) { + self->audio_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet, self); + self->audio_decoder = plm_audio_create_with_buffer(self->audio_buffer, TRUE); + } + } + + self->has_decoders = TRUE; + return TRUE; +} + +void plm_destroy(plm_t *self) { + if (self->video_decoder) { + plm_video_destroy(self->video_decoder); + } + if (self->audio_decoder) { + plm_audio_destroy(self->audio_decoder); + } + + plm_demux_destroy(self->demux); + PLM_FREE(self); +} + +int plm_get_audio_enabled(plm_t *self) { + return self->audio_enabled; +} + +int plm_has_headers(plm_t *self) { + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (!plm_init_decoders(self)) { + return FALSE; + } + + if ( + (self->video_decoder && !plm_video_has_header(self->video_decoder)) || + (self->audio_decoder && !plm_audio_has_header(self->audio_decoder)) + ) { + return FALSE; + } + + return TRUE; +} + +int plm_probe(plm_t *self, size_t probesize) { + int found_streams = plm_demux_probe(self->demux, probesize); + if (!found_streams) { + return FALSE; + } + + // Re-init decoders + self->has_decoders = FALSE; + self->video_packet_type = 0; + self->audio_packet_type = 0; + return plm_init_decoders(self); +} + +void plm_set_audio_enabled(plm_t *self, int enabled) { + self->audio_enabled = enabled; + + if (!enabled) { + self->audio_packet_type = 0; + return; + } + + self->audio_packet_type = (plm_init_decoders(self) && self->audio_decoder) + ? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index + : 0; +} + +void plm_set_audio_stream(plm_t *self, int stream_index) { + if (stream_index < 0 || stream_index > 3) { + return; + } + self->audio_stream_index = stream_index; + + // Set the correct audio_packet_type + plm_set_audio_enabled(self, self->audio_enabled); +} + +int plm_get_video_enabled(plm_t *self) { + return self->video_enabled; +} + +void plm_set_video_enabled(plm_t *self, int enabled) { + self->video_enabled = enabled; + + if (!enabled) { + self->video_packet_type = 0; + return; + } + + self->video_packet_type = (plm_init_decoders(self) && self->video_decoder) + ? PLM_DEMUX_PACKET_VIDEO_1 + : 0; +} + +int plm_get_num_video_streams(plm_t *self) { + return plm_demux_get_num_video_streams(self->demux); +} + +int plm_get_width(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_width(self->video_decoder) + : 0; +} + +int plm_get_height(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_height(self->video_decoder) + : 0; +} + +double plm_get_framerate(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_framerate(self->video_decoder) + : 0; +} + +double plm_get_pixel_aspect_ratio(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_pixel_aspect_ratio(self->video_decoder) + : 0; +} + +int plm_get_num_audio_streams(plm_t *self) { + return plm_demux_get_num_audio_streams(self->demux); +} + +int plm_get_samplerate(plm_t *self) { + return (plm_init_decoders(self) && self->audio_decoder) + ? plm_audio_get_samplerate(self->audio_decoder) + : 0; +} + +double plm_get_audio_lead_time(plm_t *self) { + return self->audio_lead_time; +} + +void plm_set_audio_lead_time(plm_t *self, double lead_time) { + self->audio_lead_time = lead_time; +} + +double plm_get_time(plm_t *self) { + return self->time; +} + +double plm_get_duration(plm_t *self) { + return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1); +} + +void plm_rewind(plm_t *self) { + if (self->video_decoder) { + plm_video_rewind(self->video_decoder); + } + + if (self->audio_decoder) { + plm_audio_rewind(self->audio_decoder); + } + + plm_demux_rewind(self->demux); + self->time = 0; + self->has_ended = FALSE; +} + +int plm_get_loop(plm_t *self) { + return self->loop; +} + +void plm_set_loop(plm_t *self, int loop) { + self->loop = loop; +} + +int plm_has_ended(plm_t *self) { + return self->has_ended; +} + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user) { + self->video_decode_callback = fp; + self->video_decode_callback_user_data = user; +} + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user) { + self->audio_decode_callback = fp; + self->audio_decode_callback_user_data = user; +} + +void plm_decode(plm_t *self, double tick) { + if (!plm_init_decoders(self)) { + return; + } + + int decode_video = (self->video_decode_callback && self->video_packet_type); + int decode_audio = (self->audio_decode_callback && self->audio_packet_type); + + if (!decode_video && !decode_audio) { + // Nothing to do here + return; + } + + int did_decode = FALSE; + int decode_video_failed = FALSE; + int decode_audio_failed = FALSE; + + double video_target_time = self->time + tick; + double audio_target_time = self->time + tick + self->audio_lead_time; + + do { + did_decode = FALSE; + + if (decode_video && plm_video_get_time(self->video_decoder) < video_target_time) { + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_video_failed = TRUE; + } + } + + if (decode_audio && plm_audio_get_time(self->audio_decoder) < audio_target_time) { + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->audio_decode_callback(self, samples, self->audio_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_audio_failed = TRUE; + } + } + } while (did_decode); + + // Did all sources we wanted to decode fail and the demuxer is at the end? + if ( + (!decode_video || decode_video_failed) && + (!decode_audio || decode_audio_failed) && + plm_demux_has_ended(self->demux) + ) { + plm_handle_end(self); + return; + } + + self->time += tick; +} + +plm_frame_t *plm_decode_video(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->time = frame->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return frame; +} + +plm_samples_t *plm_decode_audio(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->audio_packet_type) { + return NULL; + } + + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->time = samples->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return samples; +} + +void plm_handle_end(plm_t *self) { + if (self->loop) { + plm_rewind(self); + } + else { + self->has_ended = TRUE; + } +} + +void plm_read_video_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->video_packet_type); +} + +void plm_read_audio_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->audio_packet_type); +} + +void plm_read_packets(plm_t *self, int requested_type) { + plm_packet_t *packet; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if (packet->type == self->audio_packet_type) { + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + } + + if (packet->type == requested_type) { + return; + } + } + + if (plm_demux_has_ended(self->demux)) { + if (self->video_buffer) { + plm_buffer_signal_end(self->video_buffer); + } + if (self->audio_buffer) { + plm_buffer_signal_end(self->audio_buffer); + } + } +} + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + int type = self->video_packet_type; + + double start_time = plm_demux_get_start_time(self->demux, type); + double duration = plm_demux_get_duration(self->demux, type); + + if (time < 0) { + time = 0; + } + else if (time > duration) { + time = duration; + } + + plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE); + if (!packet) { + return NULL; + } + + // Disable writing to the audio buffer while decoding video + int previous_audio_packet_type = self->audio_packet_type; + self->audio_packet_type = 0; + + // Clear video buffer and decode the found packet + plm_video_rewind(self->video_decoder); + plm_video_set_time(self->video_decoder, packet->pts - start_time); + plm_buffer_write(self->video_buffer, packet->data, packet->length); + plm_frame_t *frame = plm_video_decode(self->video_decoder); + + // If we want to seek to an exact frame, we have to decode all frames + // on top of the intra frame we just jumped to. + if (seek_exact) { + while (frame && frame->time < time) { + frame = plm_video_decode(self->video_decoder); + } + } + + // Enable writing to the audio buffer again? + self->audio_packet_type = previous_audio_packet_type; + + if (frame) { + self->time = frame->time; + } + + self->has_ended = FALSE; + return frame; +} + +int plm_seek(plm_t *self, double time, int seek_exact) { + plm_frame_t *frame = plm_seek_frame(self, time, seek_exact); + + if (!frame) { + return FALSE; + } + + if (self->video_decode_callback) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + } + + // If audio is not enabled we are done here. + if (!self->audio_packet_type) { + return TRUE; + } + + // Sync up Audio. This demuxes more packets until the first audio packet + // with a PTS greater than the current time is found. plm_decode() is then + // called to decode enough audio data to satisfy the audio_lead_time. + + double start_time = plm_demux_get_start_time(self->demux, self->video_packet_type); + plm_audio_rewind(self->audio_decoder); + + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if ( + packet->type == self->audio_packet_type && + packet->pts - start_time > self->time + ) { + plm_audio_set_time(self->audio_decoder, packet->pts - start_time); + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + plm_decode(self, 0); + break; + } + } + + return TRUE; +} + + + +// ----------------------------------------------------------------------------- +// plm_buffer implementation + +enum plm_buffer_mode { + PLM_BUFFER_MODE_FILE, + PLM_BUFFER_MODE_FIXED_MEM, + PLM_BUFFER_MODE_RING, + PLM_BUFFER_MODE_APPEND +}; + +struct plm_buffer_t { + size_t bit_index; + size_t capacity; + size_t length; + size_t total_size; + int discard_read_bytes; + int has_ended; + int free_when_done; +#ifndef PLM_NO_STDIO + int close_when_done; + FILE *fh; +#endif + plm_buffer_load_callback load_callback; + plm_buffer_seek_callback seek_callback; + plm_buffer_tell_callback tell_callback; + void *load_callback_user_data; + uint8_t *bytes; + enum plm_buffer_mode mode; +}; + +typedef struct { + int16_t index; + int16_t value; +} plm_vlc_t; + +typedef struct { + int16_t index; + uint16_t value; +} plm_vlc_uint_t; + + +void plm_buffer_seek(plm_buffer_t *self, size_t pos); +size_t plm_buffer_tell(plm_buffer_t *self); +void plm_buffer_discard_read_bytes(plm_buffer_t *self); + +#ifndef PLM_NO_STDIO +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user); +void plm_buffer_seek_file_callback(plm_buffer_t *self, size_t offset, void *user); +size_t plm_buffer_tell_file_callback(plm_buffer_t *self, void *user); +#endif + +int plm_buffer_has(plm_buffer_t *self, size_t count); +int plm_buffer_read(plm_buffer_t *self, int count); +void plm_buffer_align(plm_buffer_t *self); +void plm_buffer_skip(plm_buffer_t *self, size_t count); +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v); +int plm_buffer_next_start_code(plm_buffer_t *self); +int plm_buffer_find_start_code(plm_buffer_t *self, int code); +int plm_buffer_no_start_code(plm_buffer_t *self); +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table); +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table); + +#ifndef PLM_NO_STDIO + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename) { + FILE *fh = fopen(filename, "rb"); + if (!fh) { + return NULL; + } + return plm_buffer_create_with_file(fh, TRUE); +} + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->fh = fh; + self->close_when_done = close_when_done; + self->mode = PLM_BUFFER_MODE_FILE; + self->discard_read_bytes = TRUE; + + fseek(self->fh, 0, SEEK_END); + self->total_size = ftell(self->fh); + fseek(self->fh, 0, SEEK_SET); + + self->load_callback = plm_buffer_load_file_callback; + self->seek_callback = plm_buffer_seek_file_callback; + self->tell_callback = plm_buffer_tell_file_callback; + return self; +} + +#endif // PLM_NO_STDIO + +plm_buffer_t *plm_buffer_create_with_callbacks( + plm_buffer_load_callback load_callback, + plm_buffer_seek_callback seek_callback, + plm_buffer_tell_callback tell_callback, + size_t length, + void *user +) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->mode = PLM_BUFFER_MODE_FILE; + self->total_size = length; + self->load_callback = load_callback; + self->seek_callback = seek_callback; + self->tell_callback = tell_callback; + self->load_callback_user_data = user; + return self; +} + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = length; + self->length = length; + self->total_size = length; + self->free_when_done = free_when_done; + self->bytes = bytes; + self->mode = PLM_BUFFER_MODE_FIXED_MEM; + self->discard_read_bytes = FALSE; + return self; +} + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) { + plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = capacity; + self->free_when_done = TRUE; + self->bytes = (uint8_t *)PLM_MALLOC(capacity); + self->mode = PLM_BUFFER_MODE_RING; + self->discard_read_bytes = TRUE; + return self; +} + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) { + plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity); + self->mode = PLM_BUFFER_MODE_APPEND; + self->discard_read_bytes = FALSE; + return self; +} + +void plm_buffer_destroy(plm_buffer_t *self) { +#ifndef PLM_NO_STDIO + if (self->fh && self->close_when_done) { + fclose(self->fh); + } +#endif + if (self->free_when_done) { + PLM_FREE(self->bytes); + } + PLM_FREE(self); +} + +size_t plm_buffer_get_size(plm_buffer_t *self) { + return (self->mode == PLM_BUFFER_MODE_FILE) + ? self->total_size + : self->length; +} + +size_t plm_buffer_get_remaining(plm_buffer_t *self) { + return self->length - (self->bit_index >> 3); +} + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) { + if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) { + return 0; + } + + if (self->discard_read_bytes) { + // This should be a ring buffer, but instead it just shifts all unread + // data to the beginning of the buffer and appends new data at the end. + // Seems to be good enough. + + plm_buffer_discard_read_bytes(self); + if (self->mode == PLM_BUFFER_MODE_RING) { + self->total_size = 0; + } + } + + // Do we have to resize to fit the new data? + size_t bytes_available = self->capacity - self->length; + if (bytes_available < length) { + size_t new_size = self->capacity; + do { + new_size *= 2; + } while (new_size - self->length < length); + self->bytes = (uint8_t *)PLM_REALLOC(self->bytes, new_size); + self->capacity = new_size; + } + + memcpy(self->bytes + self->length, bytes, length); + self->length += length; + self->has_ended = FALSE; + return length; +} + +void plm_buffer_signal_end(plm_buffer_t *self) { + self->total_size = self->length; +} + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user) { + self->load_callback = fp; + self->load_callback_user_data = user; +} + +void plm_buffer_rewind(plm_buffer_t *self) { + plm_buffer_seek(self, 0); +} + +void plm_buffer_seek(plm_buffer_t *self, size_t pos) { + self->has_ended = FALSE; + + if (self->seek_callback) { + self->seek_callback(self, pos, self->load_callback_user_data); + self->bit_index = 0; + self->length = 0; + } + else if (self->mode == PLM_BUFFER_MODE_RING) { + if (pos != 0) { + // Seeking to non-0 is forbidden for dynamic-mem buffers + return; + } + self->bit_index = 0; + self->length = 0; + self->total_size = 0; + } + else if (pos < self->length) { + self->bit_index = pos << 3; + } +} + +size_t plm_buffer_tell(plm_buffer_t *self) { + return self->tell_callback + ? self->tell_callback(self, self->load_callback_user_data) + (self->bit_index >> 3) - self->length + : self->bit_index >> 3; +} + +void plm_buffer_discard_read_bytes(plm_buffer_t *self) { + size_t byte_pos = self->bit_index >> 3; + if (byte_pos == self->length) { + self->bit_index = 0; + self->length = 0; + } + else if (byte_pos > 0) { + memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos); + self->bit_index -= byte_pos << 3; + self->length -= byte_pos; + } +} + +#ifndef PLM_NO_STDIO + +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + + if (self->discard_read_bytes) { + plm_buffer_discard_read_bytes(self); + } + + size_t bytes_available = self->capacity - self->length; + size_t bytes_read = fread(self->bytes + self->length, 1, bytes_available, self->fh); + self->length += bytes_read; + + if (bytes_read == 0) { + self->has_ended = TRUE; + } +} + +void plm_buffer_seek_file_callback(plm_buffer_t *self, size_t offset, void *user) { + PLM_UNUSED(user); + fseek(self->fh, offset, SEEK_SET); +} + +size_t plm_buffer_tell_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + return ftell(self->fh); +} + +#endif // PLM_NO_STDIO + +int plm_buffer_has_ended(plm_buffer_t *self) { + return self->has_ended; +} + +int plm_buffer_has(plm_buffer_t *self, size_t count) { + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + + if (self->load_callback) { + self->load_callback(self, self->load_callback_user_data); + + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + } + + if (self->total_size != 0 && self->length == self->total_size) { + self->has_ended = TRUE; + } + return FALSE; +} + +int plm_buffer_read(plm_buffer_t *self, int count) { + if (!plm_buffer_has(self, count)) { + return 0; + } + + int value = 0; + while (count) { + int current_byte = self->bytes[self->bit_index >> 3]; + + int remaining = 8 - (self->bit_index & 7); // Remaining bits in byte + int read = remaining < count ? remaining : count; // Bits in self run + int shift = remaining - read; + int mask = (0xff >> (8 - read)); + + value = (value << read) | ((current_byte & (mask << shift)) >> shift); + + self->bit_index += read; + count -= read; + } + + return value; +} + +void plm_buffer_align(plm_buffer_t *self) { + self->bit_index = ((self->bit_index + 7) >> 3) << 3; // Align to next byte +} + +void plm_buffer_skip(plm_buffer_t *self, size_t count) { + if (plm_buffer_has(self, count)) { + self->bit_index += count; + } +} + +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) { + plm_buffer_align(self); + int skipped = 0; + while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) { + self->bit_index += 8; + skipped++; + } + return skipped; +} + +int plm_buffer_next_start_code(plm_buffer_t *self) { + plm_buffer_align(self); + + while (plm_buffer_has(self, (5 << 3))) { + size_t byte_index = (self->bit_index) >> 3; + if ( + self->bytes[byte_index] == 0x00 && + self->bytes[byte_index + 1] == 0x00 && + self->bytes[byte_index + 2] == 0x01 + ) { + self->bit_index = (byte_index + 4) << 3; + return self->bytes[byte_index + 3]; + } + self->bit_index += 8; + } + return -1; +} + +int plm_buffer_find_start_code(plm_buffer_t *self, int code) { + int current = 0; + while (TRUE) { + current = plm_buffer_next_start_code(self); + if (current == code || current == -1) { + return current; + } + } + return -1; +} + +int plm_buffer_has_start_code(plm_buffer_t *self, int code) { + size_t previous_bit_index = self->bit_index; + int previous_discard_read_bytes = self->discard_read_bytes; + + self->discard_read_bytes = FALSE; + int current = plm_buffer_find_start_code(self, code); + + self->bit_index = previous_bit_index; + self->discard_read_bytes = previous_discard_read_bytes; + return current; +} + +int plm_buffer_peek_non_zero(plm_buffer_t *self, int bit_count) { + if (!plm_buffer_has(self, bit_count)) { + return FALSE; + } + + int val = plm_buffer_read(self, bit_count); + self->bit_index -= bit_count; + return val != 0; +} + +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) { + plm_vlc_t state = {0, 0}; + do { + state = table[state.index + plm_buffer_read(self, 1)]; + } while (state.index > 0); + return state.value; +} + +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table) { + return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table); +} + + + +// ---------------------------------------------------------------------------- +// plm_demux implementation + +static const int PLM_START_PACK = 0xBA; +static const int PLM_START_END = 0xB9; +static const int PLM_START_SYSTEM = 0xBB; + +struct plm_demux_t { + plm_buffer_t *buffer; + int destroy_buffer_when_done; + double system_clock_ref; + + size_t last_file_size; + double last_decoded_pts; + double start_time; + double duration; + + int start_code; + int has_pack_header; + int has_system_header; + int has_headers; + + int num_audio_streams; + int num_video_streams; + plm_packet_t current_packet; + plm_packet_t next_packet; +}; + + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos); +double plm_demux_decode_time(plm_demux_t *self); +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type); +plm_packet_t *plm_demux_get_packet(plm_demux_t *self); + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) { + plm_demux_t *self = (plm_demux_t *)PLM_MALLOC(sizeof(plm_demux_t)); + memset(self, 0, sizeof(plm_demux_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + self->start_time = PLM_PACKET_INVALID_TS; + self->duration = PLM_PACKET_INVALID_TS; + self->start_code = -1; + + plm_demux_has_headers(self); + return self; +} + +void plm_demux_destroy(plm_demux_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + PLM_FREE(self); +} + +int plm_demux_has_headers(plm_demux_t *self) { + if (self->has_headers) { + return TRUE; + } + + // Decode pack header + if (!self->has_pack_header) { + if ( + self->start_code != PLM_START_PACK && + plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_PACK; + if (!plm_buffer_has(self->buffer, 64)) { + return FALSE; + } + self->start_code = -1; + + if (plm_buffer_read(self->buffer, 4) != 0x02) { + return FALSE; + } + + self->system_clock_ref = plm_demux_decode_time(self); + plm_buffer_skip(self->buffer, 1); + plm_buffer_skip(self->buffer, 22); // mux_rate * 50 + plm_buffer_skip(self->buffer, 1); + + self->has_pack_header = TRUE; + } + + // Decode system header + if (!self->has_system_header) { + if ( + self->start_code != PLM_START_SYSTEM && + plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_SYSTEM; + if (!plm_buffer_has(self->buffer, 56)) { + return FALSE; + } + self->start_code = -1; + + plm_buffer_skip(self->buffer, 16); // header_length + plm_buffer_skip(self->buffer, 24); // rate bound + self->num_audio_streams = plm_buffer_read(self->buffer, 6); + plm_buffer_skip(self->buffer, 5); // misc flags + self->num_video_streams = plm_buffer_read(self->buffer, 5); + + self->has_system_header = TRUE; + } + + self->has_headers = TRUE; + return TRUE; +} + +int plm_demux_probe(plm_demux_t *self, size_t probesize) { + int previous_pos = plm_buffer_tell(self->buffer); + + int video_stream = FALSE; + int audio_streams[4] = {FALSE, FALSE, FALSE, FALSE}; + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if (self->start_code == PLM_DEMUX_PACKET_VIDEO_1) { + video_stream = TRUE; + } + else if ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) { + audio_streams[self->start_code - PLM_DEMUX_PACKET_AUDIO_1] = TRUE; + } + } while ( + self->start_code != -1 && + plm_buffer_tell(self->buffer) - previous_pos < probesize + ); + + self->num_video_streams = video_stream ? 1 : 0; + self->num_audio_streams = 0; + for (int i = 0; i < 4; i++) { + if (audio_streams[i]) { + self->num_audio_streams++; + } + } + + plm_demux_buffer_seek(self, previous_pos); + return (self->num_video_streams || self->num_audio_streams); +} + +int plm_demux_get_num_video_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_video_streams + : 0; +} + +int plm_demux_get_num_audio_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_audio_streams + : 0; +} + +void plm_demux_rewind(plm_demux_t *self) { + plm_buffer_rewind(self->buffer); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +int plm_demux_has_ended(plm_demux_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) { + plm_buffer_seek(self->buffer, pos); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +double plm_demux_get_start_time(plm_demux_t *self, int type) { + if (self->start_time != PLM_PACKET_INVALID_TS) { + return self->start_time; + } + + int previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find first video PTS + plm_demux_rewind(self); + do { + plm_packet_t *packet = plm_demux_decode(self); + if (!packet) { + break; + } + if (packet->type == type) { + self->start_time = packet->pts; + } + } while (self->start_time == PLM_PACKET_INVALID_TS); + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + return self->start_time; +} + +double plm_demux_get_duration(plm_demux_t *self, int type) { + size_t file_size = plm_buffer_get_size(self->buffer); + + if ( + self->duration != PLM_PACKET_INVALID_TS && + self->last_file_size == file_size + ) { + return self->duration; + } + + size_t previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find last video PTS. Start searching 64kb from the end and go further + // back if needed. + long start_range = 64 * 1024; + long max_range = 4096 * 1024; + for (long range = start_range; range <= max_range; range *= 2) { + long seek_pos = file_size - range; + if (seek_pos < 0) { + seek_pos = 0; + range = max_range; // Make sure to bail after this round + } + plm_demux_buffer_seek(self, seek_pos); + self->current_packet.length = 0; + + double last_pts = PLM_PACKET_INVALID_TS; + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self))) { + if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) { + last_pts = packet->pts; + } + } + if (last_pts != PLM_PACKET_INVALID_TS) { + self->duration = last_pts - plm_demux_get_start_time(self, type); + break; + } + } + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + self->last_file_size = file_size; + return self->duration; +} + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double seek_time, int type, int force_intra) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + // Using the current time, current byte position and the average bytes per + // second for this file, try to jump to a byte position that hopefully has + // packets containing timestamps within one second before to the desired + // seek_time. + + // If we hit close to the seek_time scan through all packets to find the + // last one (just before the seek_time) containing an intra frame. + // Otherwise we should at least be closer than before. Calculate the bytes + // per second for the jumped range and jump again. + + // The number of retries here is hard-limited to a generous amount. Usually + // the correct range is found after 1--5 jumps, even for files with very + // variable bitrates. If significantly more jumps are needed, there's + // probably something wrong with the file and we just avoid getting into an + // infinite loop. 32 retries should be enough for anybody. + + double duration = plm_demux_get_duration(self, type); + long file_size = plm_buffer_get_size(self->buffer); + long byterate = file_size / duration; + + double cur_time = self->last_decoded_pts; + double scan_span = 1; + + if (seek_time > duration) { + seek_time = duration; + } + else if (seek_time < 0) { + seek_time = 0; + } + seek_time += self->start_time; + + for (int retry = 0; retry < 32; retry++) { + int found_packet_with_pts = FALSE; + int found_packet_in_range = FALSE; + long last_valid_packet_start = -1; + double first_packet_time = PLM_PACKET_INVALID_TS; + + long cur_pos = plm_buffer_tell(self->buffer); + + // Estimate byte offset and jump to it. + long offset = (seek_time - cur_time - scan_span) * byterate; + long seek_pos = cur_pos + offset; + if (seek_pos < 0) { + seek_pos = 0; + } + else if (seek_pos > file_size - 256) { + seek_pos = file_size - 256; + } + + plm_demux_buffer_seek(self, seek_pos); + + // Scan through all packets up to the seek_time to find the last packet + // containing an intra frame. + while (plm_buffer_find_start_code(self->buffer, type) != -1) { + long packet_start = plm_buffer_tell(self->buffer); + plm_packet_t *packet = plm_demux_decode_packet(self, type); + + // Skip packet if it has no PTS + if (!packet || packet->pts == PLM_PACKET_INVALID_TS) { + continue; + } + + // Bail scanning through packets if we hit one that is outside + // seek_time - scan_span. + // We also adjust the cur_time and byterate values here so the next + // iteration can be a bit more precise. + if (packet->pts > seek_time || packet->pts < seek_time - scan_span) { + found_packet_with_pts = TRUE; + byterate = (seek_pos - cur_pos) / (packet->pts - cur_time); + cur_time = packet->pts; + break; + } + + // If we are still here, it means this packet is in close range to + // the seek_time. If this is the first packet for this jump position + // record the PTS. If we later have to back off, when there was no + // intra frame in this range, we can lower the seek_time to not scan + // this range again. + if (!found_packet_in_range) { + found_packet_in_range = TRUE; + first_packet_time = packet->pts; + } + + // Check if this is an intra frame packet. If so, record the buffer + // position of the start of this packet. We want to jump back to it + // later, when we know it's the last intra frame before desired + // seek time. + if (force_intra) { + for (size_t i = 0; i < packet->length - 6; i++) { + // Find the START_PICTURE code + if ( + packet->data[i] == 0x00 && + packet->data[i + 1] == 0x00 && + packet->data[i + 2] == 0x01 && + packet->data[i + 3] == 0x00 + ) { + // Bits 11--13 in the picture header contain the frame + // type, where 1=Intra + if ((packet->data[i + 5] & 0x38) == 8) { + last_valid_packet_start = packet_start; + } + break; + } + } + } + + // If we don't want intra frames, just use the last PTS found. + else { + last_valid_packet_start = packet_start; + } + } + + // If there was at least one intra frame in the range scanned above, + // our search is over. Jump back to the packet and decode it again. + if (last_valid_packet_start != -1) { + plm_demux_buffer_seek(self, last_valid_packet_start); + return plm_demux_decode_packet(self, type); + } + + // If we hit the right range, but still found no intra frame, we have + // to increases the scan_span. This is done exponentially to also handle + // video files with very few intra frames. + else if (found_packet_in_range) { + scan_span *= 2; + seek_time = first_packet_time; + } + + // If we didn't find any packet with a PTS, it probably means we reached + // the end of the file. Estimate byterate and cur_time accordingly. + else if (!found_packet_with_pts) { + byterate = (seek_pos - cur_pos) / (duration - cur_time); + cur_time = duration; + } + } + + return NULL; +} + +plm_packet_t *plm_demux_decode(plm_demux_t *self) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + if (self->current_packet.length) { + size_t bits_till_next_packet = self->current_packet.length << 3; + if (!plm_buffer_has(self->buffer, bits_till_next_packet)) { + return NULL; + } + plm_buffer_skip(self->buffer, bits_till_next_packet); + self->current_packet.length = 0; + } + + // Pending packet waiting for data? + if (self->next_packet.length) { + return plm_demux_get_packet(self); + } + + // Pending packet waiting for header? + if (self->start_code != -1) { + return plm_demux_decode_packet(self, self->start_code); + } + + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if ( + self->start_code == PLM_DEMUX_PACKET_VIDEO_1 || + self->start_code == PLM_DEMUX_PACKET_PRIVATE || ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) + ) { + return plm_demux_decode_packet(self, self->start_code); + } + } while (self->start_code != -1); + + return NULL; +} + +double plm_demux_decode_time(plm_demux_t *self) { + int64_t clock = plm_buffer_read(self->buffer, 3) << 30; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15) << 15; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15); + plm_buffer_skip(self->buffer, 1); + return (double)clock / 90000.0; +} + +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) { + if (!plm_buffer_has(self->buffer, 16 << 3)) { + return NULL; + } + + self->start_code = -1; + + self->next_packet.type = type; + self->next_packet.length = plm_buffer_read(self->buffer, 16); + self->next_packet.length -= plm_buffer_skip_bytes(self->buffer, 0xff); // stuffing + + // skip P-STD + if (plm_buffer_read(self->buffer, 2) == 0x01) { + plm_buffer_skip(self->buffer, 16); + self->next_packet.length -= 2; + } + + int pts_dts_marker = plm_buffer_read(self->buffer, 2); + if (pts_dts_marker == 0x03) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + plm_buffer_skip(self->buffer, 40); // skip dts + self->next_packet.length -= 10; + } + else if (pts_dts_marker == 0x02) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + self->next_packet.length -= 5; + } + else if (pts_dts_marker == 0x00) { + self->next_packet.pts = PLM_PACKET_INVALID_TS; + plm_buffer_skip(self->buffer, 4); + self->next_packet.length -= 1; + } + else { + return NULL; // invalid + } + + return plm_demux_get_packet(self); +} + +plm_packet_t *plm_demux_get_packet(plm_demux_t *self) { + if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) { + return NULL; + } + + self->current_packet.data = self->buffer->bytes + (self->buffer->bit_index >> 3); + self->current_packet.length = self->next_packet.length; + self->current_packet.type = self->next_packet.type; + self->current_packet.pts = self->next_packet.pts; + + self->next_packet.length = 0; + return &self->current_packet; +} + + + +// ----------------------------------------------------------------------------- +// plm_video implementation + +// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi +// https://sourceforge.net/projects/javampeg1video/ + +static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1; +static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2; +static const int PLM_VIDEO_PICTURE_TYPE_B = 3; + +static const int PLM_START_SEQUENCE = 0xB3; +static const int PLM_START_SLICE_FIRST = 0x01; +static const int PLM_START_SLICE_LAST = 0xAF; +static const int PLM_START_PICTURE = 0x00; +static const int PLM_START_EXTENSION = 0xB5; +static const int PLM_START_USER_DATA = 0xB2; + +#define PLM_START_IS_SLICE(c) \ + (c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST) + +static const float PLM_VIDEO_PIXEL_ASPECT_RATIO[] = { + 1.0000, /* square pixels */ + 0.6735, /* 3:4? */ + 0.7031, /* MPEG-1 / MPEG-2 video encoding divergence? */ + 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, + 1.0255, 1.0695, 1.0950, 1.1575, 1.2051, +}; + +static const double PLM_VIDEO_PICTURE_RATE[] = { + 0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940, + 60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 +}; + +static const uint8_t PLM_VIDEO_ZIG_ZAG[] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = { + 8, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +}; + +static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = { + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16 +}; + +static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = { + 32, 44, 42, 38, 32, 25, 17, 9, + 44, 62, 58, 52, 44, 35, 24, 12, + 42, 58, 55, 49, 42, 33, 23, 12, + 38, 52, 49, 44, 38, 30, 20, 10, + 32, 44, 42, 38, 32, 25, 17, 9, + 25, 35, 33, 30, 25, 20, 14, 7, + 17, 24, 23, 20, 17, 14, 9, 5, + 9, 12, 12, 10, 9, 7, 5, 2 +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = { + { 1 << 1, 0}, { 0, 1}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 3}, { 0, 2}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 5}, { 0, 4}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 7}, { 0, 6}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { 14 << 1, 0}, { 15 << 1, 0}, // 10: 0000 00x + { 16 << 1, 0}, { 17 << 1, 0}, // 11: 0000 01x + { 18 << 1, 0}, { 19 << 1, 0}, // 12: 0000 10x + { 0, 9}, { 0, 8}, // 13: 0000 11x + { -1, 0}, { 20 << 1, 0}, // 14: 0000 000x + { -1, 0}, { 21 << 1, 0}, // 15: 0000 001x + { 22 << 1, 0}, { 23 << 1, 0}, // 16: 0000 010x + { 0, 15}, { 0, 14}, // 17: 0000 011x + { 0, 13}, { 0, 12}, // 18: 0000 100x + { 0, 11}, { 0, 10}, // 19: 0000 101x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0001x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0011x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0100x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0101x + { 32 << 1, 0}, { -1, 0}, // 24: 0000 0001 0x + { -1, 0}, { 33 << 1, 0}, // 25: 0000 0001 1x + { 34 << 1, 0}, { 35 << 1, 0}, // 26: 0000 0011 0x + { 36 << 1, 0}, { 37 << 1, 0}, // 27: 0000 0011 1x + { 38 << 1, 0}, { 39 << 1, 0}, // 28: 0000 0100 0x + { 0, 21}, { 0, 20}, // 29: 0000 0100 1x + { 0, 19}, { 0, 18}, // 30: 0000 0101 0x + { 0, 17}, { 0, 16}, // 31: 0000 0101 1x + { 0, 35}, { -1, 0}, // 32: 0000 0001 00x + { -1, 0}, { 0, 34}, // 33: 0000 0001 11x + { 0, 33}, { 0, 32}, // 34: 0000 0011 00x + { 0, 31}, { 0, 30}, // 35: 0000 0011 01x + { 0, 29}, { 0, 28}, // 36: 0000 0011 10x + { 0, 27}, { 0, 26}, // 37: 0000 0011 11x + { 0, 25}, { 0, 24}, // 38: 0000 0100 00x + { 0, 23}, { 0, 22}, // 39: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = { + { 1 << 1, 0}, { 0, 0x01}, // 0: x + { -1, 0}, { 0, 0x11}, // 1: 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = { + { 1 << 1, 0}, { 0, 0x0a}, // 0: x + { 2 << 1, 0}, { 0, 0x02}, // 1: 0x + { 3 << 1, 0}, { 0, 0x08}, // 2: 00x + { 4 << 1, 0}, { 5 << 1, 0}, // 3: 000x + { 6 << 1, 0}, { 0, 0x12}, // 4: 0000x + { 0, 0x1a}, { 0, 0x01}, // 5: 0001x + { -1, 0}, { 0, 0x11}, // 6: 0000 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 0, 0x0c}, { 0, 0x0e}, // 2: 1x + { 5 << 1, 0}, { 6 << 1, 0}, // 3: 00x + { 0, 0x04}, { 0, 0x06}, // 4: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 5: 000x + { 0, 0x08}, { 0, 0x0a}, // 6: 001x + { 9 << 1, 0}, { 10 << 1, 0}, // 7: 0000x + { 0, 0x1e}, { 0, 0x01}, // 8: 0001x + { -1, 0}, { 0, 0x11}, // 9: 0000 0x + { 0, 0x16}, { 0, 0x1a}, // 10: 0000 1x +}; + +static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = { + NULL, + PLM_VIDEO_MACROBLOCK_TYPE_INTRA, + PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE, + PLM_VIDEO_MACROBLOCK_TYPE_B +}; + +static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 5 << 1, 0}, { 6 << 1, 0}, // 2: 1x + { 7 << 1, 0}, { 8 << 1, 0}, // 3: 00x + { 9 << 1, 0}, { 10 << 1, 0}, // 4: 01x + { 11 << 1, 0}, { 12 << 1, 0}, // 5: 10x + { 13 << 1, 0}, { 0, 60}, // 6: 11x + { 14 << 1, 0}, { 15 << 1, 0}, // 7: 000x + { 16 << 1, 0}, { 17 << 1, 0}, // 8: 001x + { 18 << 1, 0}, { 19 << 1, 0}, // 9: 010x + { 20 << 1, 0}, { 21 << 1, 0}, // 10: 011x + { 22 << 1, 0}, { 23 << 1, 0}, // 11: 100x + { 0, 32}, { 0, 16}, // 12: 101x + { 0, 8}, { 0, 4}, // 13: 110x + { 24 << 1, 0}, { 25 << 1, 0}, // 14: 0000x + { 26 << 1, 0}, { 27 << 1, 0}, // 15: 0001x + { 28 << 1, 0}, { 29 << 1, 0}, // 16: 0010x + { 30 << 1, 0}, { 31 << 1, 0}, // 17: 0011x + { 0, 62}, { 0, 2}, // 18: 0100x + { 0, 61}, { 0, 1}, // 19: 0101x + { 0, 56}, { 0, 52}, // 20: 0110x + { 0, 44}, { 0, 28}, // 21: 0111x + { 0, 40}, { 0, 20}, // 22: 1000x + { 0, 48}, { 0, 12}, // 23: 1001x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0x + { 34 << 1, 0}, { 35 << 1, 0}, // 25: 0000 1x + { 36 << 1, 0}, { 37 << 1, 0}, // 26: 0001 0x + { 38 << 1, 0}, { 39 << 1, 0}, // 27: 0001 1x + { 40 << 1, 0}, { 41 << 1, 0}, // 28: 0010 0x + { 42 << 1, 0}, { 43 << 1, 0}, // 29: 0010 1x + { 0, 63}, { 0, 3}, // 30: 0011 0x + { 0, 36}, { 0, 24}, // 31: 0011 1x + { 44 << 1, 0}, { 45 << 1, 0}, // 32: 0000 00x + { 46 << 1, 0}, { 47 << 1, 0}, // 33: 0000 01x + { 48 << 1, 0}, { 49 << 1, 0}, // 34: 0000 10x + { 50 << 1, 0}, { 51 << 1, 0}, // 35: 0000 11x + { 52 << 1, 0}, { 53 << 1, 0}, // 36: 0001 00x + { 54 << 1, 0}, { 55 << 1, 0}, // 37: 0001 01x + { 56 << 1, 0}, { 57 << 1, 0}, // 38: 0001 10x + { 58 << 1, 0}, { 59 << 1, 0}, // 39: 0001 11x + { 0, 34}, { 0, 18}, // 40: 0010 00x + { 0, 10}, { 0, 6}, // 41: 0010 01x + { 0, 33}, { 0, 17}, // 42: 0010 10x + { 0, 9}, { 0, 5}, // 43: 0010 11x + { -1, 0}, { 60 << 1, 0}, // 44: 0000 000x + { 61 << 1, 0}, { 62 << 1, 0}, // 45: 0000 001x + { 0, 58}, { 0, 54}, // 46: 0000 010x + { 0, 46}, { 0, 30}, // 47: 0000 011x + { 0, 57}, { 0, 53}, // 48: 0000 100x + { 0, 45}, { 0, 29}, // 49: 0000 101x + { 0, 38}, { 0, 26}, // 50: 0000 110x + { 0, 37}, { 0, 25}, // 51: 0000 111x + { 0, 43}, { 0, 23}, // 52: 0001 000x + { 0, 51}, { 0, 15}, // 53: 0001 001x + { 0, 42}, { 0, 22}, // 54: 0001 010x + { 0, 50}, { 0, 14}, // 55: 0001 011x + { 0, 41}, { 0, 21}, // 56: 0001 100x + { 0, 49}, { 0, 13}, // 57: 0001 101x + { 0, 35}, { 0, 19}, // 58: 0001 110x + { 0, 11}, { 0, 7}, // 59: 0001 111x + { 0, 39}, { 0, 27}, // 60: 0000 0001x + { 0, 59}, { 0, 55}, // 61: 0000 0010x + { 0, 47}, { 0, 31}, // 62: 0000 0011x +}; + +static const plm_vlc_t PLM_VIDEO_MOTION[] = { + { 1 << 1, 0}, { 0, 0}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 1}, { 0, -1}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 2}, { 0, -2}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 3}, { 0, -3}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { -1, 0}, { 14 << 1, 0}, // 10: 0000 00x + { 15 << 1, 0}, { 16 << 1, 0}, // 11: 0000 01x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 10x + { 0, 4}, { 0, -4}, // 13: 0000 11x + { -1, 0}, { 19 << 1, 0}, // 14: 0000 001x + { 20 << 1, 0}, { 21 << 1, 0}, // 15: 0000 010x + { 0, 7}, { 0, -7}, // 16: 0000 011x + { 0, 6}, { 0, -6}, // 17: 0000 100x + { 0, 5}, { 0, -5}, // 18: 0000 101x + { 22 << 1, 0}, { 23 << 1, 0}, // 19: 0000 0011x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0100x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0101x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0011 0x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0011 1x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0100 0x + { 0, 10}, { 0, -10}, // 25: 0000 0100 1x + { 0, 9}, { 0, -9}, // 26: 0000 0101 0x + { 0, 8}, { 0, -8}, // 27: 0000 0101 1x + { 0, 16}, { 0, -16}, // 28: 0000 0011 00x + { 0, 15}, { 0, -15}, // 29: 0000 0011 01x + { 0, 14}, { 0, -14}, // 30: 0000 0011 10x + { 0, 13}, { 0, -13}, // 31: 0000 0011 11x + { 0, 12}, { 0, -12}, // 32: 0000 0100 00x + { 0, 11}, { 0, -11}, // 33: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 1}, { 0, 2}, // 1: 0x + { 3 << 1, 0}, { 4 << 1, 0}, // 2: 1x + { 0, 0}, { 0, 3}, // 3: 10x + { 0, 4}, { 5 << 1, 0}, // 4: 11x + { 0, 5}, { 6 << 1, 0}, // 5: 111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 1x + { 0, 8}, { 9 << 1, 0}, // 8: 1111 11x + { 0, 9}, { 10 << 1, 0}, // 9: 1111 111x (MPEG-2) + { 0, 10}, { 0, 11}, // 10: 1111 1111x (MPEG-2) +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 0}, { 0, 1}, // 1: 0x + { 0, 2}, { 3 << 1, 0}, // 2: 1x + { 0, 3}, { 4 << 1, 0}, // 3: 11x + { 0, 4}, { 5 << 1, 0}, // 4: 111x + { 0, 5}, { 6 << 1, 0}, // 5: 1111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111 1x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 11x + { 0, 8}, { 9 << 1, 0}, // 8: 1111 111x + { 0, 9}, { 10 << 1, 0}, // 9: 1111 1111x (MPEG-2) + { 0, 10}, { 0, 11}, // 10: 1111 1111 1x (MPEG-2) +}; + +static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = { + PLM_VIDEO_DCT_SIZE_LUMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE +}; + + +// dct_coeff bitmap: +// 0xff00 run +// 0x00ff level + +// Decoded values are unsigned. Sign bit follows in the stream. + +static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = { + { 1 << 1, 0}, { 0, 0x0001}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 6 << 1, 0}, { 0, 0x0101}, // 3: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 4: 000x + { 9 << 1, 0}, { 10 << 1, 0}, // 5: 001x + { 0, 0x0002}, { 0, 0x0201}, // 6: 010x + { 11 << 1, 0}, { 12 << 1, 0}, // 7: 0000x + { 13 << 1, 0}, { 14 << 1, 0}, // 8: 0001x + { 15 << 1, 0}, { 0, 0x0003}, // 9: 0010x + { 0, 0x0401}, { 0, 0x0301}, // 10: 0011x + { 16 << 1, 0}, { 0, 0xffff}, // 11: 0000 0x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 1x + { 0, 0x0701}, { 0, 0x0601}, // 13: 0001 0x + { 0, 0x0102}, { 0, 0x0501}, // 14: 0001 1x + { 19 << 1, 0}, { 20 << 1, 0}, // 15: 0010 0x + { 21 << 1, 0}, { 22 << 1, 0}, // 16: 0000 00x + { 0, 0x0202}, { 0, 0x0901}, // 17: 0000 10x + { 0, 0x0004}, { 0, 0x0801}, // 18: 0000 11x + { 23 << 1, 0}, { 24 << 1, 0}, // 19: 0010 00x + { 25 << 1, 0}, { 26 << 1, 0}, // 20: 0010 01x + { 27 << 1, 0}, { 28 << 1, 0}, // 21: 0000 000x + { 29 << 1, 0}, { 30 << 1, 0}, // 22: 0000 001x + { 0, 0x0d01}, { 0, 0x0006}, // 23: 0010 000x + { 0, 0x0c01}, { 0, 0x0b01}, // 24: 0010 001x + { 0, 0x0302}, { 0, 0x0103}, // 25: 0010 010x + { 0, 0x0005}, { 0, 0x0a01}, // 26: 0010 011x + { 31 << 1, 0}, { 32 << 1, 0}, // 27: 0000 0000x + { 33 << 1, 0}, { 34 << 1, 0}, // 28: 0000 0001x + { 35 << 1, 0}, { 36 << 1, 0}, // 29: 0000 0010x + { 37 << 1, 0}, { 38 << 1, 0}, // 30: 0000 0011x + { 39 << 1, 0}, { 40 << 1, 0}, // 31: 0000 0000 0x + { 41 << 1, 0}, { 42 << 1, 0}, // 32: 0000 0000 1x + { 43 << 1, 0}, { 44 << 1, 0}, // 33: 0000 0001 0x + { 45 << 1, 0}, { 46 << 1, 0}, // 34: 0000 0001 1x + { 0, 0x1001}, { 0, 0x0502}, // 35: 0000 0010 0x + { 0, 0x0007}, { 0, 0x0203}, // 36: 0000 0010 1x + { 0, 0x0104}, { 0, 0x0f01}, // 37: 0000 0011 0x + { 0, 0x0e01}, { 0, 0x0402}, // 38: 0000 0011 1x + { 47 << 1, 0}, { 48 << 1, 0}, // 39: 0000 0000 00x + { 49 << 1, 0}, { 50 << 1, 0}, // 40: 0000 0000 01x + { 51 << 1, 0}, { 52 << 1, 0}, // 41: 0000 0000 10x + { 53 << 1, 0}, { 54 << 1, 0}, // 42: 0000 0000 11x + { 55 << 1, 0}, { 56 << 1, 0}, // 43: 0000 0001 00x + { 57 << 1, 0}, { 58 << 1, 0}, // 44: 0000 0001 01x + { 59 << 1, 0}, { 60 << 1, 0}, // 45: 0000 0001 10x + { 61 << 1, 0}, { 62 << 1, 0}, // 46: 0000 0001 11x + { -1, 0}, { 63 << 1, 0}, // 47: 0000 0000 000x + { 64 << 1, 0}, { 65 << 1, 0}, // 48: 0000 0000 001x + { 66 << 1, 0}, { 67 << 1, 0}, // 49: 0000 0000 010x + { 68 << 1, 0}, { 69 << 1, 0}, // 50: 0000 0000 011x + { 70 << 1, 0}, { 71 << 1, 0}, // 51: 0000 0000 100x + { 72 << 1, 0}, { 73 << 1, 0}, // 52: 0000 0000 101x + { 74 << 1, 0}, { 75 << 1, 0}, // 53: 0000 0000 110x + { 76 << 1, 0}, { 77 << 1, 0}, // 54: 0000 0000 111x + { 0, 0x000b}, { 0, 0x0802}, // 55: 0000 0001 000x + { 0, 0x0403}, { 0, 0x000a}, // 56: 0000 0001 001x + { 0, 0x0204}, { 0, 0x0702}, // 57: 0000 0001 010x + { 0, 0x1501}, { 0, 0x1401}, // 58: 0000 0001 011x + { 0, 0x0009}, { 0, 0x1301}, // 59: 0000 0001 100x + { 0, 0x1201}, { 0, 0x0105}, // 60: 0000 0001 101x + { 0, 0x0303}, { 0, 0x0008}, // 61: 0000 0001 110x + { 0, 0x0602}, { 0, 0x1101}, // 62: 0000 0001 111x + { 78 << 1, 0}, { 79 << 1, 0}, // 63: 0000 0000 0001x + { 80 << 1, 0}, { 81 << 1, 0}, // 64: 0000 0000 0010x + { 82 << 1, 0}, { 83 << 1, 0}, // 65: 0000 0000 0011x + { 84 << 1, 0}, { 85 << 1, 0}, // 66: 0000 0000 0100x + { 86 << 1, 0}, { 87 << 1, 0}, // 67: 0000 0000 0101x + { 88 << 1, 0}, { 89 << 1, 0}, // 68: 0000 0000 0110x + { 90 << 1, 0}, { 91 << 1, 0}, // 69: 0000 0000 0111x + { 0, 0x0a02}, { 0, 0x0902}, // 70: 0000 0000 1000x + { 0, 0x0503}, { 0, 0x0304}, // 71: 0000 0000 1001x + { 0, 0x0205}, { 0, 0x0107}, // 72: 0000 0000 1010x + { 0, 0x0106}, { 0, 0x000f}, // 73: 0000 0000 1011x + { 0, 0x000e}, { 0, 0x000d}, // 74: 0000 0000 1100x + { 0, 0x000c}, { 0, 0x1a01}, // 75: 0000 0000 1101x + { 0, 0x1901}, { 0, 0x1801}, // 76: 0000 0000 1110x + { 0, 0x1701}, { 0, 0x1601}, // 77: 0000 0000 1111x + { 92 << 1, 0}, { 93 << 1, 0}, // 78: 0000 0000 0001 0x + { 94 << 1, 0}, { 95 << 1, 0}, // 79: 0000 0000 0001 1x + { 96 << 1, 0}, { 97 << 1, 0}, // 80: 0000 0000 0010 0x + { 98 << 1, 0}, { 99 << 1, 0}, // 81: 0000 0000 0010 1x + {100 << 1, 0}, {101 << 1, 0}, // 82: 0000 0000 0011 0x + {102 << 1, 0}, {103 << 1, 0}, // 83: 0000 0000 0011 1x + { 0, 0x001f}, { 0, 0x001e}, // 84: 0000 0000 0100 0x + { 0, 0x001d}, { 0, 0x001c}, // 85: 0000 0000 0100 1x + { 0, 0x001b}, { 0, 0x001a}, // 86: 0000 0000 0101 0x + { 0, 0x0019}, { 0, 0x0018}, // 87: 0000 0000 0101 1x + { 0, 0x0017}, { 0, 0x0016}, // 88: 0000 0000 0110 0x + { 0, 0x0015}, { 0, 0x0014}, // 89: 0000 0000 0110 1x + { 0, 0x0013}, { 0, 0x0012}, // 90: 0000 0000 0111 0x + { 0, 0x0011}, { 0, 0x0010}, // 91: 0000 0000 0111 1x + {104 << 1, 0}, {105 << 1, 0}, // 92: 0000 0000 0001 00x + {106 << 1, 0}, {107 << 1, 0}, // 93: 0000 0000 0001 01x + {108 << 1, 0}, {109 << 1, 0}, // 94: 0000 0000 0001 10x + {110 << 1, 0}, {111 << 1, 0}, // 95: 0000 0000 0001 11x + { 0, 0x0028}, { 0, 0x0027}, // 96: 0000 0000 0010 00x + { 0, 0x0026}, { 0, 0x0025}, // 97: 0000 0000 0010 01x + { 0, 0x0024}, { 0, 0x0023}, // 98: 0000 0000 0010 10x + { 0, 0x0022}, { 0, 0x0021}, // 99: 0000 0000 0010 11x + { 0, 0x0020}, { 0, 0x010e}, // 100: 0000 0000 0011 00x + { 0, 0x010d}, { 0, 0x010c}, // 101: 0000 0000 0011 01x + { 0, 0x010b}, { 0, 0x010a}, // 102: 0000 0000 0011 10x + { 0, 0x0109}, { 0, 0x0108}, // 103: 0000 0000 0011 11x + { 0, 0x0112}, { 0, 0x0111}, // 104: 0000 0000 0001 000x + { 0, 0x0110}, { 0, 0x010f}, // 105: 0000 0000 0001 001x + { 0, 0x0603}, { 0, 0x1002}, // 106: 0000 0000 0001 010x + { 0, 0x0f02}, { 0, 0x0e02}, // 107: 0000 0000 0001 011x + { 0, 0x0d02}, { 0, 0x0c02}, // 108: 0000 0000 0001 100x + { 0, 0x0b02}, { 0, 0x1f01}, // 109: 0000 0000 0001 101x + { 0, 0x1e01}, { 0, 0x1d01}, // 110: 0000 0000 0001 110x + { 0, 0x1c01}, { 0, 0x1b01}, // 111: 0000 0000 0001 111x +}; + +typedef struct { + int full_px; + int is_set; + int r_size; + int r_size_v; // MPEG-2: vertical f_code - 1 + int h; + int v; +} plm_video_motion_t; + +struct plm_video_t { + double framerate; + double pixel_aspect_ratio; + double time; + int frames_decoded; + int width; + int height; + int mb_width; + int mb_height; + int mb_size; + + int luma_width; + int luma_height; + + int chroma_width; + int chroma_height; + + int start_code; + int picture_type; + + plm_video_motion_t motion_forward; + plm_video_motion_t motion_backward; + + int has_sequence_header; + + int quantizer_scale; + int slice_begin; + int macroblock_address; + + int mb_row; + int mb_col; + + int macroblock_type; + int macroblock_intra; + + int dc_predictor[3]; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + plm_frame_t frame_current; + plm_frame_t frame_forward; + plm_frame_t frame_backward; + + uint8_t *frames_data; + + int block_data[64]; + uint8_t intra_quant_matrix[64]; + uint8_t non_intra_quant_matrix[64]; + + int has_reference_frame; + int assume_no_b_frames; + + // MPEG-2 + int mpeg2; + int intra_dc_precision; + int q_scale_type; + int intra_vlc_format; + int alternate_scan; + int quantizer_scale_code; +}; + +static inline uint8_t plm_clamp(int n) { + if (n > 255) { + n = 255; + } + else if (n < 0) { + n = 0; + } + return n; +} + +int plm_video_decode_sequence_header(plm_video_t *self); +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base); +void plm_video_decode_picture(plm_video_t *self); +void plm_video_decode_extension(plm_video_t *self); +int plm_video_decode_coeff_b15(plm_video_t *self, int *run, int *level); +void plm_video_decode_slice(plm_video_t *self, int slice); +void plm_video_decode_macroblock(plm_video_t *self); +void plm_video_decode_motion_vectors(plm_video_t *self); +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion); +void plm_video_predict_macroblock(plm_video_t *self); +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_process_macroblock(plm_video_t *self, uint8_t *s, uint8_t *d, int mh, int mb, int bs, int interp); +void plm_video_decode_block(plm_video_t *self, int block); +void plm_video_idct(int *block); + +plm_video_t * plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_video_t *self = (plm_video_t *)PLM_MALLOC(sizeof(plm_video_t)); + memset(self, 0, sizeof(plm_video_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + // Attempt to decode the sequence header + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + if (self->start_code != -1) { + plm_video_decode_sequence_header(self); + } + return self; +} + +void plm_video_destroy(plm_video_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + + if (self->has_sequence_header) { + PLM_FREE(self->frames_data); + } + + PLM_FREE(self); +} + +double plm_video_get_framerate(plm_video_t *self) { + return plm_video_has_header(self) + ? self->framerate + : 0; +} + +double plm_video_get_pixel_aspect_ratio(plm_video_t *self) { + return plm_video_has_header(self) + ? self->pixel_aspect_ratio + : 0; +} + +int plm_video_get_width(plm_video_t *self) { + return plm_video_has_header(self) + ? self->width + : 0; +} + +int plm_video_get_height(plm_video_t *self) { + return plm_video_has_header(self) + ? self->height + : 0; +} + +void plm_video_set_no_delay(plm_video_t *self, int no_delay) { + self->assume_no_b_frames = no_delay; +} + +double plm_video_get_time(plm_video_t *self) { + return self->time; +} + +void plm_video_set_time(plm_video_t *self, double time) { + self->frames_decoded = self->framerate * time; + self->time = time; +} + +void plm_video_rewind(plm_video_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->frames_decoded = 0; + self->has_reference_frame = FALSE; + self->start_code = -1; +} + +int plm_video_has_ended(plm_video_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_frame_t *plm_video_decode(plm_video_t *self) { + if (!plm_video_has_header(self)) { + return NULL; + } + + plm_frame_t *frame = NULL; + do { + if (self->start_code != PLM_START_PICTURE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE); + + if (self->start_code == -1) { + // If we reached the end of the file and the previously decoded + // frame was a reference frame, we still have to return it. + if ( + self->has_reference_frame && + !self->assume_no_b_frames && + plm_buffer_has_ended(self->buffer) && ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) + ) { + self->has_reference_frame = FALSE; + frame = &self->frame_backward; + break; + } + + return NULL; + } + } + + // Make sure we have a full picture in the buffer before attempting to + // decode it. Sadly, this can only be done by seeking for the start code + // of the next picture. Also, if we didn't find the start code for the + // next picture, but the source has ended, we assume that this last + // picture is in the buffer. + if ( + plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 && + !plm_buffer_has_ended(self->buffer) + ) { + return NULL; + } + plm_buffer_discard_read_bytes(self->buffer); + + plm_video_decode_picture(self); + + if (self->assume_no_b_frames) { + frame = &self->frame_backward; + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + frame = &self->frame_current; + } + else if (self->has_reference_frame) { + frame = &self->frame_forward; + } + else { + self->has_reference_frame = TRUE; + } + } while (!frame); + + frame->time = self->time; + self->frames_decoded++; + self->time = (double)self->frames_decoded / self->framerate; + + return frame; +} + +int plm_video_has_header(plm_video_t *self) { + if (self->has_sequence_header) { + return TRUE; + } + + if (self->start_code != PLM_START_SEQUENCE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + } + if (self->start_code == -1) { + return FALSE; + } + + if (!plm_video_decode_sequence_header(self)) { + return FALSE; + } + + return TRUE; +} + +int plm_video_decode_sequence_header(plm_video_t *self) { + int max_header_size = 64 + 2 * 64 * 8; // 64 bit header + 2x 64 byte matrix + if (!plm_buffer_has(self->buffer, max_header_size)) { + return FALSE; + } + + self->width = plm_buffer_read(self->buffer, 12); + self->height = plm_buffer_read(self->buffer, 12); + + if (self->width <= 0 || self->height <= 0) { + return FALSE; + } + + // Get pixel aspect ratio + int pixel_aspect_ratio_code; + pixel_aspect_ratio_code = plm_buffer_read(self->buffer, 4); + pixel_aspect_ratio_code -= 1; + if (pixel_aspect_ratio_code < 0) { + pixel_aspect_ratio_code = 0; + } + int par_last = (sizeof(PLM_VIDEO_PIXEL_ASPECT_RATIO) / + sizeof(PLM_VIDEO_PIXEL_ASPECT_RATIO[0]) - 1); + if (pixel_aspect_ratio_code > par_last) { + pixel_aspect_ratio_code = par_last; + } + self->pixel_aspect_ratio = + PLM_VIDEO_PIXEL_ASPECT_RATIO[pixel_aspect_ratio_code]; + + // Get frame rate + self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)]; + + // Skip bit_rate, marker, buffer_size and constrained bit + plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1); + + // Load custom intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64); + } + + // Load custom non intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->non_intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX, 64); + } + + self->mb_width = (self->width + 15) >> 4; + self->mb_height = (self->height + 15) >> 4; + self->mb_size = self->mb_width * self->mb_height; + + self->luma_width = self->mb_width << 4; + self->luma_height = self->mb_height << 4; + + self->chroma_width = self->mb_width << 3; + self->chroma_height = self->mb_height << 3; + + + // Allocate one big chunk of data for all 3 frames = 9 planes + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size); + + self->frames_data = (uint8_t*)PLM_MALLOC(frame_data_size * 3); + plm_video_init_frame(self, &self->frame_current, self->frames_data + frame_data_size * 0); + plm_video_init_frame(self, &self->frame_forward, self->frames_data + frame_data_size * 1); + plm_video_init_frame(self, &self->frame_backward, self->frames_data + frame_data_size * 2); + + self->has_sequence_header = TRUE; + return TRUE; +} + +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base) { + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + + frame->width = self->width; + frame->height = self->height; + frame->y.width = self->luma_width; + frame->y.height = self->luma_height; + frame->y.data = base; + + frame->cr.width = self->chroma_width; + frame->cr.height = self->chroma_height; + frame->cr.data = base + luma_plane_size; + + frame->cb.width = self->chroma_width; + frame->cb.height = self->chroma_height; + frame->cb.data = base + luma_plane_size + chroma_plane_size; +} + +void plm_video_decode_picture(plm_video_t *self) { + plm_buffer_skip(self->buffer, 10); // skip temporalReference + self->picture_type = plm_buffer_read(self->buffer, 3); + plm_buffer_skip(self->buffer, 16); // skip vbv_delay + + // D frames or unknown coding type + if (self->picture_type <= 0 || self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) { + return; + } + + // Forward full_px, f_code + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_B + ) { + self->motion_forward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_forward.r_size = f_code - 1; + } + + // Backward full_px, f_code + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + self->motion_backward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_backward.r_size = f_code - 1; + } + + plm_frame_t frame_temp = self->frame_forward; + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_forward = self->frame_backward; + } + + + // Find first slice start code; MPEG-2: read the picture coding and quant matrix extensions + if (!self->mpeg2) { + self->motion_forward.r_size_v = self->motion_forward.r_size; + self->motion_backward.r_size_v = self->motion_backward.r_size; + } + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if (self->start_code == PLM_START_EXTENSION) { + plm_video_decode_extension(self); + } + } while ( + self->start_code == PLM_START_EXTENSION || + self->start_code == PLM_START_USER_DATA + ); + + // Decode all slices + while (PLM_START_IS_SLICE(self->start_code)) { + plm_video_decode_slice(self, self->start_code & 0x000000FF); + if (self->macroblock_address >= self->mb_size - 1) { + break; + } + self->start_code = plm_buffer_next_start_code(self->buffer); + } + + // If this is a reference picture rotate the prediction pointers + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_backward = self->frame_current; + self->frame_current = frame_temp; + } +} + +static const uint8_t PLM_VIDEO_ALT_SCAN[] = { // MPEG-2 alternate scan (alternate_scan = 1) + 0, 8, 16, 24, 1, 9, 2, 10, 17, 25, 32, 40, 48, 56, 57, 49, + 41, 33, 26, 18, 3, 11, 4, 12, 19, 27, 34, 42, 50, 58, 35, 43, + 51, 59, 20, 28, 5, 13, 6, 14, 21, 29, 36, 44, 52, 60, 37, 45, + 53, 61, 22, 30, 7, 15, 23, 31, 38, 46, 54, 62, 39, 47, 55, 63 +}; +static const uint8_t PLM_VIDEO_NONLINEAR_QSCALE[32] = { // MPEG-2 q_scale_type = 1 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, + 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 104, 112 +}; + +// MPEG-2: quantiser_scale from quantiser_scale_code, in MPEG-2 units (MPEG-1: twice the code) +static int plm_video_qscale(plm_video_t *self, int code) { + self->quantizer_scale_code = code; + if (!self->mpeg2) return code * 2; + return self->q_scale_type ? PLM_VIDEO_NONLINEAR_QSCALE[code] : code * 2; +} + +void plm_video_decode_extension(plm_video_t *self) { + int id = plm_buffer_read(self->buffer, 4); + if (id == 1) { // sequence extension + self->mpeg2 = TRUE; + plm_buffer_skip(self->buffer, 8 + 1 + 2 + 2 + 2 + 12 + 1 + 8 + 1 + 2 + 5); + } + else if (id == 8) { // picture coding extension + self->mpeg2 = TRUE; + int f00 = plm_buffer_read(self->buffer, 4), f01 = plm_buffer_read(self->buffer, 4); + int f10 = plm_buffer_read(self->buffer, 4), f11 = plm_buffer_read(self->buffer, 4); + self->motion_forward.r_size = f00 - 1; self->motion_forward.r_size_v = f01 - 1; + self->motion_backward.r_size = f10 - 1; self->motion_backward.r_size_v = f11 - 1; + self->motion_forward.full_px = self->motion_backward.full_px = 0; + self->intra_dc_precision = plm_buffer_read(self->buffer, 2); + plm_buffer_skip(self->buffer, 2 + 1 + 1 + 1); // picture_structure, top_field_first, frame_pred_frame_dct, concealment_mv + self->q_scale_type = plm_buffer_read(self->buffer, 1); + self->intra_vlc_format = plm_buffer_read(self->buffer, 1); + self->alternate_scan = plm_buffer_read(self->buffer, 1); + } + else if (id == 3) { // quant matrix extension + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) self->intra_quant_matrix[PLM_VIDEO_ZIG_ZAG[i]] = plm_buffer_read(self->buffer, 8); + } + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) self->non_intra_quant_matrix[PLM_VIDEO_ZIG_ZAG[i]] = plm_buffer_read(self->buffer, 8); + } + } +} + +void plm_video_decode_slice(plm_video_t *self, int slice) { + self->slice_begin = TRUE; + self->macroblock_address = (slice - 1) * self->mb_width - 1; + + // Reset motion vectors and DC predictors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + self->dc_predictor[0] = self->dc_predictor[1] = self->dc_predictor[2] = 128 << self->intra_dc_precision; + + self->quantizer_scale = plm_video_qscale(self, plm_buffer_read(self->buffer, 5)); + + // Skip extra + while (plm_buffer_read(self->buffer, 1)) { + plm_buffer_skip(self->buffer, 8); + } + + do { + plm_video_decode_macroblock(self); + } while ( + self->macroblock_address < self->mb_size - 1 && + plm_buffer_peek_non_zero(self->buffer, 23) + ); +} + +void plm_video_decode_macroblock(plm_video_t *self) { + // Decode increment + int increment = 0; + int t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + + while (t == 34) { + // macroblock_stuffing + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + while (t == 35) { + // macroblock_escape + increment += 33; + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + increment += t; + + // Process any skipped macroblocks + if (self->slice_begin) { + // The first increment of each slice is relative to beginning of the + // previous row, not the previous macroblock + self->slice_begin = FALSE; + self->macroblock_address += increment; + } + else { + if (self->macroblock_address + increment >= self->mb_size) { + return; // invalid + } + if (increment > 1) { + // Skipped macroblocks reset DC predictors + self->dc_predictor[0] = self->dc_predictor[1] = self->dc_predictor[2] = 128 << self->intra_dc_precision; + + // Skipped macroblocks in P-pictures reset motion vectors + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + } + + // Predict skipped macroblocks + while (increment > 1) { + self->macroblock_address++; + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + plm_video_predict_macroblock(self); + increment--; + } + self->macroblock_address++; + } + + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) { + return; // corrupt stream; + } + + // Process the current macroblock + const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type]; + self->macroblock_type = plm_buffer_read_vlc(self->buffer, table); + + self->macroblock_intra = (self->macroblock_type & 0x01); + self->motion_forward.is_set = (self->macroblock_type & 0x08); + self->motion_backward.is_set = (self->macroblock_type & 0x04); + + // Quantizer scale + if ((self->macroblock_type & 0x10) != 0) { + self->quantizer_scale = plm_video_qscale(self, plm_buffer_read(self->buffer, 5)); + } + + if (self->macroblock_intra) { + // Intra-coded macroblocks reset motion vectors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + } + else { + // Non-intra macroblocks reset DC predictors + self->dc_predictor[0] = self->dc_predictor[1] = self->dc_predictor[2] = 128 << self->intra_dc_precision; + + plm_video_decode_motion_vectors(self); + plm_video_predict_macroblock(self); + } + + // Decode blocks + int cbp = ((self->macroblock_type & 0x02) != 0) + ? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN) + : (self->macroblock_intra ? 0x3f : 0); + + for (int block = 0, mask = 0x20; block < 6; block++) { + if ((cbp & mask) != 0) { + plm_video_decode_block(self, block); + } + mask >>= 1; + } +} + +void plm_video_decode_motion_vectors(plm_video_t *self) { + + // Forward + if (self->motion_forward.is_set) { + self->motion_forward.h = plm_video_decode_motion_vector(self, self->motion_forward.r_size, self->motion_forward.h); + self->motion_forward.v = plm_video_decode_motion_vector(self, self->motion_forward.r_size_v, self->motion_forward.v); + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + // No motion information in P-picture, reset vectors + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + + if (self->motion_backward.is_set) { + self->motion_backward.h = plm_video_decode_motion_vector(self, self->motion_backward.r_size, self->motion_backward.h); + self->motion_backward.v = plm_video_decode_motion_vector(self, self->motion_backward.r_size_v, self->motion_backward.v); + } +} + +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) { + int fscale = 1 << r_size; + int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION); + int r = 0; + int d; + + if ((m_code != 0) && (fscale != 1)) { + r = plm_buffer_read(self->buffer, r_size); + d = ((abs(m_code) - 1) << r_size) + r + 1; + if (m_code < 0) { + d = -d; + } + } + else { + d = m_code; + } + + motion += d; + if (motion > (fscale << 4) - 1) { + motion -= fscale << 5; + } + else if (motion < (int)((unsigned)(-fscale) << 4)) { + motion += fscale << 5; + } + + return motion; +} + +void plm_video_predict_macroblock(plm_video_t *self) { + int fw_h = self->motion_forward.h; + int fw_v = self->motion_forward.v; + + if (self->motion_forward.full_px) { + fw_h <<= 1; + fw_v <<= 1; + } + + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + int bw_h = self->motion_backward.h; + int bw_v = self->motion_backward.v; + + if (self->motion_backward.full_px) { + bw_h <<= 1; + bw_v <<= 1; + } + + if (self->motion_forward.is_set) { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + if (self->motion_backward.is_set) { + plm_video_interpolate_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + } +} + +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, FALSE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, FALSE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, FALSE); +} + +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, TRUE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, TRUE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, TRUE); +} + +#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \ + int dest_scan = DEST_WIDTH - BLOCK_SIZE; \ + int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \ + for (int y = 0; y < BLOCK_SIZE; y++) { \ + for (int x = 0; x < BLOCK_SIZE; x++) { \ + DEST[DEST_INDEX] = OP; \ + SOURCE_INDEX++; DEST_INDEX++; \ + } \ + SOURCE_INDEX += source_scan; \ + DEST_INDEX += dest_scan; \ + }} while(FALSE) + +void plm_video_process_macroblock( + plm_video_t *self, uint8_t *s, uint8_t *d, + int motion_h, int motion_v, int block_size, int interpolate +) { + int dw = self->mb_width * block_size; + + int hp = motion_h >> 1; + int vp = motion_v >> 1; + int odd_h = (motion_h & 1) == 1; + int odd_v = (motion_v & 1) == 1; + + unsigned int si = ((self->mb_row * block_size) + vp) * dw + (self->mb_col * block_size) + hp; + unsigned int di = (self->mb_row * dw + self->mb_col) * block_size; + + unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size); + if (si > max_address || di > max_address) { + return; // corrupt video + } + + #define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \ + case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \ + PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \ + break + + switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) { + PLM_MB_CASE(0, 0, 0, (s[si])); + PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1); + PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1); + PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2); + + PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1); + PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1); + } + + #undef PLM_MB_CASE +} + +// MPEG-2 table B.15 (intra_vlc_format = 1): {code, length} for the 111 run/level pairs, then escape and EOB +static const uint16_t PLM_B15_CODES[111 + 2][2] = { + {0x02, 2}, {0x06, 3}, {0x07, 4}, {0x1c, 5}, {0x1d, 5}, {0x05, 6}, {0x04, 6}, {0x7b, 7}, + {0x7c, 7}, {0x23, 8}, {0x22, 8}, {0xfa, 8}, {0xfb, 8}, {0xfe, 8}, {0xff, 8}, {0x1f,14}, + {0x1e,14}, {0x1d,14}, {0x1c,14}, {0x1b,14}, {0x1a,14}, {0x19,14}, {0x18,14}, {0x17,14}, + {0x16,14}, {0x15,14}, {0x14,14}, {0x13,14}, {0x12,14}, {0x11,14}, {0x10,14}, {0x18,15}, + {0x17,15}, {0x16,15}, {0x15,15}, {0x14,15}, {0x13,15}, {0x12,15}, {0x11,15}, {0x10,15}, + {0x02, 3}, {0x06, 5}, {0x79, 7}, {0x27, 8}, {0x20, 8}, {0x16,13}, {0x15,13}, {0x1f,15}, + {0x1e,15}, {0x1d,15}, {0x1c,15}, {0x1b,15}, {0x1a,15}, {0x19,15}, {0x13,16}, {0x12,16}, + {0x11,16}, {0x10,16}, {0x05, 5}, {0x07, 7}, {0xfc, 8}, {0x0c,10}, {0x14,13}, {0x07, 5}, + {0x26, 8}, {0x1c,12}, {0x13,13}, {0x06, 6}, {0xfd, 8}, {0x12,12}, {0x07, 6}, {0x04, 9}, + {0x12,13}, {0x06, 7}, {0x1e,12}, {0x14,16}, {0x04, 7}, {0x15,12}, {0x05, 7}, {0x11,12}, + {0x78, 7}, {0x11,13}, {0x7a, 7}, {0x10,13}, {0x21, 8}, {0x1a,16}, {0x25, 8}, {0x19,16}, + {0x24, 8}, {0x18,16}, {0x05, 9}, {0x17,16}, {0x07, 9}, {0x16,16}, {0x0d,10}, {0x15,16}, + {0x1f,12}, {0x1a,12}, {0x19,12}, {0x17,12}, {0x16,12}, {0x1f,13}, {0x1e,13}, {0x1d,13}, + {0x1c,13}, {0x1b,13}, {0x1f,16}, {0x1e,16}, {0x1d,16}, {0x1c,16}, {0x1b,16}, + {0x01, 6}, {0x06, 4} +}; +static const uint8_t PLM_B14_LEVEL[111] = { + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32, + 33,34,35,36,37,38,39,40,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,1,2,3,4,5,1, + 2,3,4,1,2,3,1,2,3,1,2,3,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +}; +static const uint8_t PLM_B14_RUN[111] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,3, + 3,3,3,4,4,4,5,5,5,6,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,16,16, + 17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, +}; +static int16_t plm_b15_trie[2048][2]; // node -> child per bit; leaf = -(entry+1) +static int plm_b15_nodes; + +static void plm_b15_build(void) { + if (plm_b15_nodes) return; + plm_b15_nodes = 1; + for (int e = 0; e < 113; e++) { + int code = PLM_B15_CODES[e][0], len = PLM_B15_CODES[e][1], node = 0; + for (int b = len - 1; b >= 0; b--) { + int bit = (code >> b) & 1; + if (b == 0) { plm_b15_trie[node][bit] = (int16_t)(-(e + 1)); break; } + if (plm_b15_trie[node][bit] <= 0) plm_b15_trie[node][bit] = (int16_t)(plm_b15_nodes++); + node = plm_b15_trie[node][bit]; + } + } +} + +// returns 0 = run/level, 1 = escape, 2 = end of block +int plm_video_decode_coeff_b15(plm_video_t *self, int *run, int *level) { + int node = 0; + plm_b15_build(); + for (int i = 0; i < 17; i++) { + int nx = plm_b15_trie[node][plm_buffer_read(self->buffer, 1)]; + if (nx < 0) { + int e = -nx - 1; + if (e == 111) return 1; + if (e == 112) return 2; + *run = PLM_B14_RUN[e]; + *level = PLM_B14_LEVEL[e]; + return 0; + } + if (nx == 0) break; // invalid code + node = nx; + } + return 2; +} + +void plm_video_decode_block(plm_video_t *self, int block) { + + int n = 0; + uint8_t *quant_matrix; + const uint8_t *scan = self->alternate_scan ? PLM_VIDEO_ALT_SCAN : PLM_VIDEO_ZIG_ZAG; + int mismatch = 0, f63 = 0; // MPEG-2 mismatch control + + // Decode DC coefficient of intra-coded blocks + if (self->macroblock_intra) { + int predictor; + int dct_size; + + // DC prediction + int plane_index = block > 3 ? block - 3 : 0; + predictor = self->dc_predictor[plane_index]; + dct_size = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]); + + // Read DC coeff + if (dct_size > 0) { + int differential = plm_buffer_read(self->buffer, dct_size); + if ((differential & (1 << (dct_size - 1))) != 0) { + self->block_data[0] = predictor + differential; + } + else { + self->block_data[0] = predictor + (-(1 << dct_size) | (differential + 1)); + } + } + else { + self->block_data[0] = predictor; + } + + // Save predictor value + self->dc_predictor[plane_index] = self->block_data[0]; + + // Dequantize + premultiply (MPEG-2: dc * (8 >> intra_dc_precision)) + mismatch = self->block_data[0] << (3 - self->intra_dc_precision); + self->block_data[0] = mismatch << 5; + + quant_matrix = self->intra_quant_matrix; + n = 1; + } + else { + quant_matrix = self->non_intra_quant_matrix; + } + + // Decode AC coefficients (+DC for non-intra) + int level = 0; + while (TRUE) { + int run = 0; + int escape = 0; + if (self->mpeg2 && self->macroblock_intra && self->intra_vlc_format) { + int r = plm_video_decode_coeff_b15(self, &run, &level); + if (r == 2) break; // end_of_block + if (r == 1) escape = 1; + else if (plm_buffer_read(self->buffer, 1)) level = -level; + } + else { + uint16_t coeff = plm_buffer_read_vlc_uint(self->buffer, PLM_VIDEO_DCT_COEFF); + + if ((coeff == 0x0001) && (n > 0) && (plm_buffer_read(self->buffer, 1) == 0)) { + // end_of_block + break; + } + if (coeff == 0xffff) { + escape = 1; + } + else { + run = coeff >> 8; + level = coeff & 0xff; + if (plm_buffer_read(self->buffer, 1)) { + level = -level; + } + } + } + if (escape) { + run = plm_buffer_read(self->buffer, 6); + if (self->mpeg2) { // MPEG-2: 12-bit signed level + level = plm_buffer_read(self->buffer, 12); + if (level & 0x800) level -= 0x1000; + } + else { + level = plm_buffer_read(self->buffer, 8); + if (level == 0) { + level = plm_buffer_read(self->buffer, 8); + } + else if (level == 128) { + level = plm_buffer_read(self->buffer, 8) - 256; + } + else if (level > 128) { + level = level - 256; + } + } + } + + n += run; + if (n < 0 || n >= 64) { + return; // invalid + } + + int de_zig_zagged = scan[n]; + n++; + + if (self->mpeg2) { + // MPEG-2: F = (2*QF [+ sign for non-intra]) * W * qs / 32, truncated toward zero, saturated + int a = level < 0 ? -level : level; + a = self->macroblock_intra ? (2 * a) : (2 * a + 1); + a = (a * quant_matrix[de_zig_zagged] * self->quantizer_scale) >> 5; + level = level < 0 ? -a : a; + if (level > 2047) level = 2047; + else if (level < -2048) level = -2048; + mismatch += level; + if (de_zig_zagged == 63) f63 = level; + } + else { + // Dequantize, oddify, clip (MPEG-1; quantizer_scale is kept in MPEG-2 units) + level = (unsigned)level << 1; + if (!self->macroblock_intra) { + level += (level < 0 ? -1 : 1); + } + level = (level * (self->quantizer_scale >> 1) * quant_matrix[de_zig_zagged]) >> 4; + if ((level & 1) == 0) { + level -= level > 0 ? 1 : -1; + } + if (level > 2047) { + level = 2047; + } + else if (level < -2048) { + level = -2048; + } + } + + // Save premultiplied coefficient + self->block_data[de_zig_zagged] = level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged]; + } + + // MPEG-2 mismatch control: an even coefficient sum toggles the LSB of F[7][7] + if (self->mpeg2 && (mismatch & 1) == 0) { + f63 = (f63 & 1) ? f63 - 1 : f63 + 1; + self->block_data[63] = f63 * PLM_VIDEO_PREMULTIPLIER_MATRIX[63]; + n = 64; // force the full IDCT + } + + // Move block to its place + uint8_t *d; + int dw; + int di; + + if (block < 4) { + d = self->frame_current.y.data; + dw = self->luma_width; + di = (self->mb_row * self->luma_width + self->mb_col) << 4; + if ((block & 1) != 0) { + di += 8; + } + if ((block & 2) != 0) { + di += self->luma_width << 3; + } + } + else { + d = (block == 4) ? self->frame_current.cb.data : self->frame_current.cr.data; + dw = self->chroma_width; + di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3); + } + + int *s = self->block_data; + int si = 0; + if (self->macroblock_intra) { + // Overwrite (no prediction) + if (n == 1) { + int clamped = plm_clamp((s[0] + 128) >> 8); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } + else { + // Add data to the predicted macroblock + if (n == 1) { + int value = (s[0] + 128) >> 8; + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value)); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } +} + +void plm_video_idct(int *block) { + int + b1, b3, b4, b6, b7, tmp1, tmp2, m0, + x0, x1, x2, x3, x4, y3, y4, y5, y6, y7; + + // Transform columns + for (int i = 0; i < 8; ++i) { + b1 = block[4 * 8 + i]; + b3 = block[2 * 8 + i] + block[6 * 8 + i]; + b4 = block[5 * 8 + i] - block[3 * 8 + i]; + tmp1 = block[1 * 8 + i] + block[7 * 8 + i]; + tmp2 = block[3 * 8 + i] + block[5 * 8 + i]; + b6 = block[1 * 8 + i] - block[7 * 8 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 * 8 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 * 8 + i] = b7 + y4; + block[1 * 8 + i] = x4 + y3; + block[2 * 8 + i] = y5 - x0; + block[3 * 8 + i] = y6 - y7; + block[4 * 8 + i] = y6 + y7; + block[5 * 8 + i] = x0 + y5; + block[6 * 8 + i] = y3 - x4; + block[7 * 8 + i] = y4 - b7; + } + + // Transform rows + for (int i = 0; i < 64; i += 8) { + b1 = block[4 + i]; + b3 = block[2 + i] + block[6 + i]; + b4 = block[5 + i] - block[3 + i]; + tmp1 = block[1 + i] + block[7 + i]; + tmp2 = block[3 + i] + block[5 + i]; + b6 = block[1 + i] - block[7 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 + i] = (b7 + y4 + 128) >> 8; + block[1 + i] = (x4 + y3 + 128) >> 8; + block[2 + i] = (y5 - x0 + 128) >> 8; + block[3 + i] = (y6 - y7 + 128) >> 8; + block[4 + i] = (y6 + y7 + 128) >> 8; + block[5 + i] = (x0 + y5 + 128) >> 8; + block[6 + i] = (y3 - x4 + 128) >> 8; + block[7 + i] = (y4 - b7 + 128) >> 8; + } +} + +// YCbCr conversion following the BT.601 standard: +// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion + +#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET) \ + y = ((frame->y.data[y_index + Y_OFFSET]-16) * 76309) >> 16; \ + dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r); \ + dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g); \ + dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b); + +#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \ + void NAME(plm_frame_t *frame, uint8_t *dest, int stride) { \ + int cols = frame->width >> 1; \ + int rows = frame->height >> 1; \ + int yw = frame->y.width; \ + int cw = frame->cb.width; \ + for (int row = 0; row < rows; row++) { \ + int c_index = row * cw; \ + int y_index = row * 2 * yw; \ + int d_index = row * 2 * stride; \ + for (int col = 0; col < cols; col++) { \ + int y; \ + int cr = frame->cr.data[c_index] - 128; \ + int cb = frame->cb.data[c_index] - 128; \ + int r = (cr * 104597) >> 16; \ + int g = (cb * 25674 + cr * 53278) >> 16; \ + int b = (cb * 132201) >> 16; \ + PLM_PUT_PIXEL(RI, GI, BI, 0, 0); \ + PLM_PUT_PIXEL(RI, GI, BI, 1, BYTES_PER_PIXEL); \ + PLM_PUT_PIXEL(RI, GI, BI, yw, stride); \ + PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \ + c_index += 1; \ + y_index += 2; \ + d_index += 2 * BYTES_PER_PIXEL; \ + } \ + } \ + } + +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb, 3, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr, 3, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1) + + +#undef PLM_PUT_PIXEL +#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION + + + +// ----------------------------------------------------------------------------- +// plm_audio implementation + +// Based on kjmp2 by Martin J. Fiedler +// http://keyj.emphy.de/kjmp2/ + +static const int PLM_AUDIO_FRAME_SYNC = 0x7ff; + +static const int PLM_AUDIO_MPEG_2_5 = 0x0; +static const int PLM_AUDIO_MPEG_2 = 0x2; +static const int PLM_AUDIO_MPEG_1 = 0x3; + +static const int PLM_AUDIO_LAYER_III = 0x1; +static const int PLM_AUDIO_LAYER_II = 0x2; +static const int PLM_AUDIO_LAYER_I = 0x3; + +static const int PLM_AUDIO_MODE_STEREO = 0x0; +static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1; +static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2; +static const int PLM_AUDIO_MODE_MONO = 0x3; + +static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = { + 44100, 48000, 32000, 0, // MPEG-1 + 22050, 24000, 16000, 0 // MPEG-2 +}; + +static const short PLM_AUDIO_BIT_RATE[] = { + 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // MPEG-1 + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 // MPEG-2 +}; + +static const int PLM_AUDIO_SCALEFACTOR_BASE[] = { + 0x02000000, 0x01965FEA, 0x01428A30 +}; + +static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = { + 0.0, -0.5, -0.5, -0.5, -0.5, -0.5, + -0.5, -1.0, -1.0, -1.0, -1.0, -1.5, + -1.5, -2.0, -2.0, -2.5, -2.5, -3.0, + -3.5, -3.5, -4.0, -4.5, -5.0, -5.5, + -6.5, -7.0, -8.0, -8.5, -9.5, -10.5, + -12.0, -13.0, -14.5, -15.5, -17.5, -19.0, + -20.5, -22.5, -24.5, -26.5, -29.0, -31.5, + -34.0, -36.5, -39.5, -42.5, -45.5, -48.5, + -52.0, -55.5, -58.5, -62.5, -66.0, -69.5, + -73.5, -77.0, -80.5, -84.5, -88.0, -91.5, + -95.0, -98.0, -101.0, -104.0, 106.5, 109.0, + 111.0, 112.5, 113.5, 114.0, 114.0, 113.5, + 112.0, 110.5, 107.5, 104.0, 100.0, 94.5, + 88.5, 81.5, 73.0, 63.5, 53.0, 41.5, + 28.5, 14.5, -1.0, -18.0, -36.0, -55.5, + -76.5, -98.5, -122.0, -147.0, -173.5, -200.5, + -229.5, -259.5, -290.5, -322.5, -355.5, -389.5, + -424.0, -459.5, -495.5, -532.0, -568.5, -605.0, + -641.5, -678.0, -714.0, -749.0, -783.5, -817.0, + -849.0, -879.5, -908.5, -935.0, -959.5, -981.0, + -1000.5, -1016.0, -1028.5, -1037.5, -1042.5, -1043.5, + -1040.0, -1031.5, 1018.5, 1000.0, 976.0, 946.5, + 911.0, 869.5, 822.0, 767.5, 707.0, 640.0, + 565.5, 485.0, 397.0, 302.5, 201.0, 92.5, + -22.5, -144.0, -272.5, -407.0, -547.5, -694.0, + -846.0, -1003.0, -1165.0, -1331.5, -1502.0, -1675.5, + -1852.5, -2031.5, -2212.5, -2394.0, -2576.5, -2758.5, + -2939.5, -3118.5, -3294.5, -3467.5, -3635.5, -3798.5, + -3955.0, -4104.5, -4245.5, -4377.5, -4499.0, -4609.5, + -4708.0, -4792.5, -4863.5, -4919.0, -4958.0, -4979.5, + -4983.0, -4967.5, -4931.5, -4875.0, -4796.0, -4694.5, + -4569.5, -4420.0, -4246.0, -4046.0, -3820.0, -3567.0, + 3287.0, 2979.5, 2644.0, 2280.5, 1888.0, 1467.5, + 1018.5, 541.0, 35.0, -499.0, -1061.0, -1650.0, + -2266.5, -2909.0, -3577.0, -4270.0, -4987.5, -5727.5, + -6490.0, -7274.0, -8077.5, -8899.5, -9739.0, -10594.5, + -11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, + -16895.5, -17820.0, -18744.5, -19668.0, -20588.0, -21503.0, + -22410.5, -23308.5, -24195.0, -25068.5, -25926.5, -26767.0, + -27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0, + -32009.5, -32645.0, -33247.0, -33814.5, -34346.0, -34839.5, + -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0, + -37156.5, -37315.0, -37428.0, -37496.0, 37519.0, 37496.0, + 37428.0, 37315.0, 37156.5, 36954.0, 36707.5, 36417.5, + 36084.5, 35710.0, 35295.0, 34839.5, 34346.0, 33814.5, + 33247.0, 32645.0, 32009.5, 31342.0, 30644.5, 29919.0, + 29166.5, 28389.0, 27589.0, 26767.0, 25926.5, 25068.5, + 24195.0, 23308.5, 22410.5, 21503.0, 20588.0, 19668.0, + 18744.5, 17820.0, 16895.5, 15973.5, 15056.0, 14144.5, + 13241.0, 12347.0, 11464.5, 10594.5, 9739.0, 8899.5, + 8077.5, 7274.0, 6490.0, 5727.5, 4987.5, 4270.0, + 3577.0, 2909.0, 2266.5, 1650.0, 1061.0, 499.0, + -35.0, -541.0, -1018.5, -1467.5, -1888.0, -2280.5, + -2644.0, -2979.5, 3287.0, 3567.0, 3820.0, 4046.0, + 4246.0, 4420.0, 4569.5, 4694.5, 4796.0, 4875.0, + 4931.5, 4967.5, 4983.0, 4979.5, 4958.0, 4919.0, + 4863.5, 4792.5, 4708.0, 4609.5, 4499.0, 4377.5, + 4245.5, 4104.5, 3955.0, 3798.5, 3635.5, 3467.5, + 3294.5, 3118.5, 2939.5, 2758.5, 2576.5, 2394.0, + 2212.5, 2031.5, 1852.5, 1675.5, 1502.0, 1331.5, + 1165.0, 1003.0, 846.0, 694.0, 547.5, 407.0, + 272.5, 144.0, 22.5, -92.5, -201.0, -302.5, + -397.0, -485.0, -565.5, -640.0, -707.0, -767.5, + -822.0, -869.5, -911.0, -946.5, -976.0, -1000.0, + 1018.5, 1031.5, 1040.0, 1043.5, 1042.5, 1037.5, + 1028.5, 1016.0, 1000.5, 981.0, 959.5, 935.0, + 908.5, 879.5, 849.0, 817.0, 783.5, 749.0, + 714.0, 678.0, 641.5, 605.0, 568.5, 532.0, + 495.5, 459.5, 424.0, 389.5, 355.5, 322.5, + 290.5, 259.5, 229.5, 200.5, 173.5, 147.0, + 122.0, 98.5, 76.5, 55.5, 36.0, 18.0, + 1.0, -14.5, -28.5, -41.5, -53.0, -63.5, + -73.0, -81.5, -88.5, -94.5, -100.0, -104.0, + -107.5, -110.5, -112.0, -113.5, -114.0, -114.0, + -113.5, -112.5, -111.0, -109.0, 106.5, 104.0, + 101.0, 98.0, 95.0, 91.5, 88.0, 84.5, + 80.5, 77.0, 73.5, 69.5, 66.0, 62.5, + 58.5, 55.5, 52.0, 48.5, 45.5, 42.5, + 39.5, 36.5, 34.0, 31.5, 29.0, 26.5, + 24.5, 22.5, 20.5, 19.0, 17.5, 15.5, + 14.5, 13.0, 12.0, 10.5, 9.5, 8.5, + 8.0, 7.0, 6.5, 5.5, 5.0, 4.5, + 4.0, 3.5, 3.5, 3.0, 2.5, 2.5, + 2.0, 2.0, 1.5, 1.5, 1.0, 1.0, + 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5 +}; + +// Quantizer lookup, step 1: bitrate classes +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = { + // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate + { 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2 }, // mono + // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan + { 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2 } // stereo +}; + +// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit +#define PLM_AUDIO_QUANT_TAB_A (27 | 64) // Table 3-B.2a: high-rate, sblimit = 27 +#define PLM_AUDIO_QUANT_TAB_B (30 | 64) // Table 3-B.2b: high-rate, sblimit = 30 +#define PLM_AUDIO_QUANT_TAB_C 8 // Table 3-B.2c: low-rate, sblimit = 8 +#define PLM_AUDIO_QUANT_TAB_D 12 // Table 3-B.2d: low-rate, sblimit = 12 + +static const uint8_t QUANT_LUT_STEP_2[3][3] = { + //44.1 kHz, 48 kHz, 32 kHz + { PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_D }, // 32 - 48 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A }, // 56 - 80 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_B } // 96+ kbit/sec/ch +}; + +// Quantizer lookup, step 3: B2 table, subband -> nbal, row index +// (upper 4 bits: nbal, lower 4 bits: row index) +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = { + // Low-rate table (3-B.2c and 3-B.2d) + { + 0x44,0x44, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34 + }, + // High-rate table (3-B.2a and 3-B.2b) + { + 0x43,0x43,0x43, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20 + }, + // MPEG-2 LSR table (B.2 in ISO 13818-3) + { + 0x45,0x45,0x45,0x45, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24 + } +}; + +// Quantizer lookup, step 4: table row, allocation[] value -> quant table index +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_4[6][16] = { + { 0, 1, 2, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17 }, + { 0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }, + { 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } +}; + +typedef struct plm_quantizer_spec_t { + unsigned short levels; + unsigned char group; + unsigned char bits; +} plm_quantizer_spec_t; + +static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = { + { 3, 1, 5 }, // 1 + { 5, 1, 7 }, // 2 + { 7, 0, 3 }, // 3 + { 9, 1, 10 }, // 4 + { 15, 0, 4 }, // 5 + { 31, 0, 5 }, // 6 + { 63, 0, 6 }, // 7 + { 127, 0, 7 }, // 8 + { 255, 0, 8 }, // 9 + { 511, 0, 9 }, // 10 + { 1023, 0, 10 }, // 11 + { 2047, 0, 11 }, // 12 + { 4095, 0, 12 }, // 13 + { 8191, 0, 13 }, // 14 + { 16383, 0, 14 }, // 15 + { 32767, 0, 15 }, // 16 + { 65535, 0, 16 } // 17 +}; + +struct plm_audio_t { + double time; + int samples_decoded; + int samplerate_index; + int bitrate_index; + int version; + int layer; + int mode; + int bound; + int v_pos; + int next_frame_data_size; + int has_header; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + const plm_quantizer_spec_t *allocation[2][32]; + uint8_t scale_factor_info[2][32]; + int scale_factor[2][32][3]; + int sample[2][32][3]; + + plm_samples_t samples; + float D[1024]; + float V[2][1024]; + float U[32]; +}; + +int plm_audio_find_frame_sync(plm_audio_t *self); +int plm_audio_decode_header(plm_audio_t *self); +void plm_audio_decode_frame(plm_audio_t *self); +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3); +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part); +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp); + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_audio_t *self = (plm_audio_t *)PLM_MALLOC(sizeof(plm_audio_t)); + memset(self, 0, sizeof(plm_audio_t)); + + self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME; + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + self->samplerate_index = 3; // Indicates 0 + + memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + + // Attempt to decode first header + self->next_frame_data_size = plm_audio_decode_header(self); + + return self; +} + +void plm_audio_destroy(plm_audio_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + PLM_FREE(self); +} + +int plm_audio_has_header(plm_audio_t *self) { + if (self->has_header) { + return TRUE; + } + + self->next_frame_data_size = plm_audio_decode_header(self); + return self->has_header; +} + +int plm_audio_get_samplerate(plm_audio_t *self) { + return plm_audio_has_header(self) + ? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index] + : 0; +} + +double plm_audio_get_time(plm_audio_t *self) { + return self->time; +} + +void plm_audio_set_time(plm_audio_t *self, double time) { + self->samples_decoded = time * + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + self->time = time; +} + +void plm_audio_rewind(plm_audio_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->samples_decoded = 0; + self->next_frame_data_size = 0; +} + +int plm_audio_has_ended(plm_audio_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_samples_t *plm_audio_decode(plm_audio_t *self) { + // Do we have at least enough information to decode the frame header? + if (!self->next_frame_data_size) { + if (!plm_buffer_has(self->buffer, 48)) { + return NULL; + } + self->next_frame_data_size = plm_audio_decode_header(self); + } + + if ( + self->next_frame_data_size == 0 || + !plm_buffer_has(self->buffer, self->next_frame_data_size << 3) + ) { + return NULL; + } + + plm_audio_decode_frame(self); + self->next_frame_data_size = 0; + + self->samples.time = self->time; + + self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME; + self->time = (double)self->samples_decoded / + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + + return &self->samples; +} + +int plm_audio_find_frame_sync(plm_audio_t *self) { + size_t i; + for (i = self->buffer->bit_index >> 3; i < self->buffer->length-1; i++) { + if ( + self->buffer->bytes[i] == 0xFF && + (self->buffer->bytes[i+1] & 0xFE) == 0xFC + ) { + self->buffer->bit_index = ((i+1) << 3) + 3; + return TRUE; + } + } + self->buffer->bit_index = (i + 1) << 3; + return FALSE; +} + +int plm_audio_decode_header(plm_audio_t *self) { + if (!plm_buffer_has(self->buffer, 48)) { + return 0; + } + + plm_buffer_skip_bytes(self->buffer, 0x00); + int sync = plm_buffer_read(self->buffer, 11); + + + // Attempt to resync if no syncword was found. This sucks balls. The MP2 + // stream contains a syncword just before every frame (11 bits set to 1). + // However, this syncword is not guaranteed to not occur elsewhere in the + // stream. So, if we have to resync, we also have to check if the header + // (samplerate, bitrate) differs from the one we had before. This all + // may still lead to garbage data being decoded :/ + + if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) { + return 0; + } + + self->version = plm_buffer_read(self->buffer, 2); + self->layer = plm_buffer_read(self->buffer, 2); + int hasCRC = !plm_buffer_read(self->buffer, 1); + + if ( + self->version != PLM_AUDIO_MPEG_1 || + self->layer != PLM_AUDIO_LAYER_II + ) { + return 0; + } + + int bitrate_index = plm_buffer_read(self->buffer, 4) - 1; + if (bitrate_index > 13) { + return 0; + } + + int samplerate_index = plm_buffer_read(self->buffer, 2); + if (samplerate_index == 3) { + return 0; + } + + int padding = plm_buffer_read(self->buffer, 1); + plm_buffer_skip(self->buffer, 1); // f_private + int mode = plm_buffer_read(self->buffer, 2); + + // If we already have a header, make sure the samplerate, bitrate and mode + // are still the same, otherwise we might have missed sync. + if ( + self->has_header && ( + self->bitrate_index != bitrate_index || + self->samplerate_index != samplerate_index || + self->mode != mode + ) + ) { + return 0; + } + + self->bitrate_index = bitrate_index; + self->samplerate_index = samplerate_index; + self->mode = mode; + self->has_header = TRUE; + + // Parse the mode_extension, set up the stereo bound + if (mode == PLM_AUDIO_MODE_JOINT_STEREO) { + self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2; + } + else { + plm_buffer_skip(self->buffer, 2); + self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32; + } + + // Discard the last 4 bits of the header and the CRC value, if present + plm_buffer_skip(self->buffer, 4); // copyright(1), original(1), emphasis(2) + if (hasCRC) { + plm_buffer_skip(self->buffer, 16); + } + + // Compute frame size, check if we have enough data to decode the whole + // frame. + int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index]; + int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + int frame_size = (144000 * bitrate / samplerate) + padding; + return frame_size - (hasCRC ? 6 : 4); +} + +void plm_audio_decode_frame(plm_audio_t *self) { + // Prepare the quantizer table lookups + int tab3 = 0; + int sblimit = 0; + + int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1; + int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index]; + tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index]; + sblimit = tab3 & 63; + tab3 >>= 6; + + if (self->bound > sblimit) { + self->bound = sblimit; + } + + // Read the allocation information + for (int sb = 0; sb < self->bound; sb++) { + self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3); + self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3); + } + + for (int sb = self->bound; sb < sblimit; sb++) { + self->allocation[0][sb] = + self->allocation[1][sb] = + plm_audio_read_allocation(self, sb, tab3); + } + + // Read scale factor selector information + int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2; + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + self->scale_factor_info[ch][sb] = plm_buffer_read(self->buffer, 2); + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb]; + } + } + + // Read scale factors + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + int *sf = self->scale_factor[ch][sb]; + switch (self->scale_factor_info[ch][sb]) { + case 0: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 1: + sf[0] = + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 2: + sf[0] = + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 3: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + } + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0]; + self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1]; + self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2]; + } + } + + // Coefficient input and reconstruction + int out_pos = 0; + for (int part = 0; part < 3; part++) { + for (int granule = 0; granule < 4; granule++) { + + // Read the samples + for (int sb = 0; sb < self->bound; sb++) { + plm_audio_read_samples(self, 0, sb, part); + plm_audio_read_samples(self, 1, sb, part); + } + for (int sb = self->bound; sb < sblimit; sb++) { + plm_audio_read_samples(self, 0, sb, part); + self->sample[1][sb][0] = self->sample[0][sb][0]; + self->sample[1][sb][1] = self->sample[0][sb][1]; + self->sample[1][sb][2] = self->sample[0][sb][2]; + } + for (int sb = sblimit; sb < 32; sb++) { + self->sample[0][sb][0] = 0; + self->sample[0][sb][1] = 0; + self->sample[0][sb][2] = 0; + self->sample[1][sb][0] = 0; + self->sample[1][sb][1] = 0; + self->sample[1][sb][2] = 0; + } + + // Synthesis loop + for (int p = 0; p < 3; p++) { + // Shifting step + self->v_pos = (self->v_pos - 64) & 1023; + + for (int ch = 0; ch < 2; ch++) { + plm_audio_idct36(self->sample[ch], p, self->V[ch], self->v_pos); + + // Build U, windowing, calculate output + memset(self->U, 0, sizeof(self->U)); + + int d_index = 512 - (self->v_pos >> 1); + int v_index = (self->v_pos % 128) >> 1; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + d_index -= (512 - 32); + v_index = (128 - 32 + 1024) - v_index; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + // Output samples + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float *out_channel = ch == 0 + ? self->samples.left + : self->samples.right; + for (int j = 0; j < 32; j++) { + out_channel[out_pos + j] = self->U[j] / -1090519040.0f; + } + #else + for (int j = 0; j < 32; j++) { + self->samples.interleaved[((out_pos + j) << 1) + ch] = + self->U[j] / -1090519040.0f; + } + #endif + } // End of synthesis channel loop + out_pos += 32; + } // End of synthesis sub-block loop + + } // Decoding of the granule finished + } + + plm_buffer_align(self->buffer); +} + +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3) { + int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb]; + int qtab = PLM_AUDIO_QUANT_LUT_STEP_4[tab4 & 15][plm_buffer_read(self->buffer, tab4 >> 4)]; + return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0; +} + +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) { + const plm_quantizer_spec_t *q = self->allocation[ch][sb]; + int sf = self->scale_factor[ch][sb][part]; + int *sample = self->sample[ch][sb]; + int val = 0; + + if (!q) { + // No bits allocated for this subband + sample[0] = sample[1] = sample[2] = 0; + return; + } + + // Resolve scalefactor + if (sf == 63) { + sf = 0; + } + else { + int shift = (sf / 3) | 0; + sf = (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift; + } + + // Decode samples + int adj = q->levels; + if (q->group) { + // Decode grouped samples + val = plm_buffer_read(self->buffer, q->bits); + sample[0] = val % adj; + val /= adj; + sample[1] = val % adj; + sample[2] = val / adj; + } + else { + // Decode direct samples + sample[0] = plm_buffer_read(self->buffer, q->bits); + sample[1] = plm_buffer_read(self->buffer, q->bits); + sample[2] = plm_buffer_read(self->buffer, q->bits); + } + + // Postmultiply samples + int scale = 65536 / (adj + 1); + adj = ((adj + 1) >> 1) - 1; + + val = (adj - sample[0]) * scale; + sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[1]) * scale; + sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[2]) * scale; + sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; +} + +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp) { + float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, + t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24, + t25, t26, t27, t28, t29, t30, t31, t32, t33; + + t01 = (float)(s[0][ss] + s[31][ss]); t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f; + t03 = (float)(s[1][ss] + s[30][ss]); t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f; + t05 = (float)(s[2][ss] + s[29][ss]); t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f; + t07 = (float)(s[3][ss] + s[28][ss]); t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f; + t09 = (float)(s[4][ss] + s[27][ss]); t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f; + t11 = (float)(s[5][ss] + s[26][ss]); t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f; + t13 = (float)(s[6][ss] + s[25][ss]); t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f; + t15 = (float)(s[7][ss] + s[24][ss]); t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f; + t17 = (float)(s[8][ss] + s[23][ss]); t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f; + t19 = (float)(s[9][ss] + s[22][ss]); t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f; + t21 = (float)(s[10][ss] + s[21][ss]); t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f; + t23 = (float)(s[11][ss] + s[20][ss]); t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f; + t25 = (float)(s[12][ss] + s[19][ss]); t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f; + t27 = (float)(s[13][ss] + s[18][ss]); t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f; + t29 = (float)(s[14][ss] + s[17][ss]); t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f; + t31 = (float)(s[15][ss] + s[16][ss]); t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f; + + t33 = t01 + t31; t31 = (t01 - t31) * 0.502419286188f; + t01 = t03 + t29; t29 = (t03 - t29) * 0.52249861494f; + t03 = t05 + t27; t27 = (t05 - t27) * 0.566944034816f; + t05 = t07 + t25; t25 = (t07 - t25) * 0.64682178336f; + t07 = t09 + t23; t23 = (t09 - t23) * 0.788154623451f; + t09 = t11 + t21; t21 = (t11 - t21) * 1.06067768599f; + t11 = t13 + t19; t19 = (t13 - t19) * 1.72244709824f; + t13 = t15 + t17; t17 = (t15 - t17) * 5.10114861869f; + t15 = t33 + t13; t13 = (t33 - t13) * 0.509795579104f; + t33 = t01 + t11; t01 = (t01 - t11) * 0.601344886935f; + t11 = t03 + t09; t09 = (t03 - t09) * 0.899976223136f; + t03 = t05 + t07; t07 = (t05 - t07) * 2.56291544774f; + t05 = t15 + t03; t15 = (t15 - t03) * 0.541196100146f; + t03 = t33 + t11; t11 = (t33 - t11) * 1.30656296488f; + t33 = t05 + t03; t05 = (t05 - t03) * 0.707106781187f; + t03 = t15 + t11; t15 = (t15 - t11) * 0.707106781187f; + t03 += t15; + t11 = t13 + t07; t13 = (t13 - t07) * 0.541196100146f; + t07 = t01 + t09; t09 = (t01 - t09) * 1.30656296488f; + t01 = t11 + t07; t07 = (t11 - t07) * 0.707106781187f; + t11 = t13 + t09; t13 = (t13 - t09) * 0.707106781187f; + t11 += t13; t01 += t11; + t11 += t07; t07 += t13; + t09 = t31 + t17; t31 = (t31 - t17) * 0.509795579104f; + t17 = t29 + t19; t29 = (t29 - t19) * 0.601344886935f; + t19 = t27 + t21; t21 = (t27 - t21) * 0.899976223136f; + t27 = t25 + t23; t23 = (t25 - t23) * 2.56291544774f; + t25 = t09 + t27; t09 = (t09 - t27) * 0.541196100146f; + t27 = t17 + t19; t19 = (t17 - t19) * 1.30656296488f; + t17 = t25 + t27; t27 = (t25 - t27) * 0.707106781187f; + t25 = t09 + t19; t19 = (t09 - t19) * 0.707106781187f; + t25 += t19; + t09 = t31 + t23; t31 = (t31 - t23) * 0.541196100146f; + t23 = t29 + t21; t21 = (t29 - t21) * 1.30656296488f; + t29 = t09 + t23; t23 = (t09 - t23) * 0.707106781187f; + t09 = t31 + t21; t31 = (t31 - t21) * 0.707106781187f; + t09 += t31; t29 += t09; t09 += t23; t23 += t31; + t17 += t29; t29 += t25; t25 += t09; t09 += t27; + t27 += t23; t23 += t19; t19 += t31; + t21 = t02 + t32; t02 = (t02 - t32) * 0.502419286188f; + t32 = t04 + t30; t04 = (t04 - t30) * 0.52249861494f; + t30 = t06 + t28; t28 = (t06 - t28) * 0.566944034816f; + t06 = t08 + t26; t08 = (t08 - t26) * 0.64682178336f; + t26 = t10 + t24; t10 = (t10 - t24) * 0.788154623451f; + t24 = t12 + t22; t22 = (t12 - t22) * 1.06067768599f; + t12 = t14 + t20; t20 = (t14 - t20) * 1.72244709824f; + t14 = t16 + t18; t16 = (t16 - t18) * 5.10114861869f; + t18 = t21 + t14; t14 = (t21 - t14) * 0.509795579104f; + t21 = t32 + t12; t32 = (t32 - t12) * 0.601344886935f; + t12 = t30 + t24; t24 = (t30 - t24) * 0.899976223136f; + t30 = t06 + t26; t26 = (t06 - t26) * 2.56291544774f; + t06 = t18 + t30; t18 = (t18 - t30) * 0.541196100146f; + t30 = t21 + t12; t12 = (t21 - t12) * 1.30656296488f; + t21 = t06 + t30; t30 = (t06 - t30) * 0.707106781187f; + t06 = t18 + t12; t12 = (t18 - t12) * 0.707106781187f; + t06 += t12; + t18 = t14 + t26; t26 = (t14 - t26) * 0.541196100146f; + t14 = t32 + t24; t24 = (t32 - t24) * 1.30656296488f; + t32 = t18 + t14; t14 = (t18 - t14) * 0.707106781187f; + t18 = t26 + t24; t24 = (t26 - t24) * 0.707106781187f; + t18 += t24; t32 += t18; + t18 += t14; t26 = t14 + t24; + t14 = t02 + t16; t02 = (t02 - t16) * 0.509795579104f; + t16 = t04 + t20; t04 = (t04 - t20) * 0.601344886935f; + t20 = t28 + t22; t22 = (t28 - t22) * 0.899976223136f; + t28 = t08 + t10; t10 = (t08 - t10) * 2.56291544774f; + t08 = t14 + t28; t14 = (t14 - t28) * 0.541196100146f; + t28 = t16 + t20; t20 = (t16 - t20) * 1.30656296488f; + t16 = t08 + t28; t28 = (t08 - t28) * 0.707106781187f; + t08 = t14 + t20; t20 = (t14 - t20) * 0.707106781187f; + t08 += t20; + t14 = t02 + t10; t02 = (t02 - t10) * 0.541196100146f; + t10 = t04 + t22; t22 = (t04 - t22) * 1.30656296488f; + t04 = t14 + t10; t10 = (t14 - t10) * 0.707106781187f; + t14 = t02 + t22; t02 = (t02 - t22) * 0.707106781187f; + t14 += t02; t04 += t14; t14 += t10; t10 += t02; + t16 += t04; t04 += t08; t08 += t14; t14 += t28; + t28 += t10; t10 += t20; t20 += t02; t21 += t16; + t16 += t32; t32 += t04; t04 += t06; t06 += t08; + t08 += t18; t18 += t14; t14 += t30; t30 += t28; + t28 += t26; t26 += t10; t10 += t12; t12 += t20; + t20 += t24; t24 += t02; + + d[dp + 48] = -t33; + d[dp + 49] = d[dp + 47] = -t21; + d[dp + 50] = d[dp + 46] = -t17; + d[dp + 51] = d[dp + 45] = -t16; + d[dp + 52] = d[dp + 44] = -t01; + d[dp + 53] = d[dp + 43] = -t32; + d[dp + 54] = d[dp + 42] = -t29; + d[dp + 55] = d[dp + 41] = -t04; + d[dp + 56] = d[dp + 40] = -t03; + d[dp + 57] = d[dp + 39] = -t06; + d[dp + 58] = d[dp + 38] = -t25; + d[dp + 59] = d[dp + 37] = -t08; + d[dp + 60] = d[dp + 36] = -t11; + d[dp + 61] = d[dp + 35] = -t18; + d[dp + 62] = d[dp + 34] = -t09; + d[dp + 63] = d[dp + 33] = -t14; + d[dp + 32] = -t05; + d[dp + 0] = t05; d[dp + 31] = -t30; + d[dp + 1] = t30; d[dp + 30] = -t27; + d[dp + 2] = t27; d[dp + 29] = -t28; + d[dp + 3] = t28; d[dp + 28] = -t07; + d[dp + 4] = t07; d[dp + 27] = -t26; + d[dp + 5] = t26; d[dp + 26] = -t23; + d[dp + 6] = t23; d[dp + 25] = -t10; + d[dp + 7] = t10; d[dp + 24] = -t15; + d[dp + 8] = t15; d[dp + 23] = -t12; + d[dp + 9] = t12; d[dp + 22] = -t19; + d[dp + 10] = t19; d[dp + 21] = -t20; + d[dp + 11] = t20; d[dp + 20] = -t13; + d[dp + 12] = t13; d[dp + 19] = -t24; + d[dp + 13] = t24; d[dp + 18] = -t31; + d[dp + 14] = t31; d[dp + 17] = -t02; + d[dp + 15] = t02; d[dp + 16] = 0.0; +} + + +#endif // PL_MPEG_IMPLEMENTATION diff --git a/games/rac1/host/src/movie/player.c b/games/rac1/host/src/movie/player.c new file mode 100644 index 0000000..9b99fde --- /dev/null +++ b/games/rac1/host/src/movie/player.c @@ -0,0 +1,73 @@ +/* Movie playback (RAC1 Wrapper). + * + * The game plays its movies (the intro, the cut-scenes) with Sony's sample + * player (src/game/movie: disc streaming, the IPU, EE threads, the SPU), + * entered through func_0023B670 (host/overrides). On the PC the whole call is + * replaced: the movie's sectors are read from the disc image, the MPEG-2 + * video is decoded with pl_mpeg2.h and shown at the movie's frame rate with + * its sound (PS2 ADPCM, the language track the game asks for). A skip key + * ends it early, like a pad button on the PS2. Shared with the UYA Wrapper. */ +#include +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg2.h" +#include "pss.h" +#include "uya_runtime.h" + +int uya_movie_play_track(uint32_t lsn, uint32_t bytes, int track) +{ + uint32_t sectors = (bytes + 2047) / 2048; + uint8_t *ps = malloc((size_t)sectors * 2048), *es = 0; + int skipped = 0, frames = 0; + if (!ps || uya_disc_read(lsn, sectors, ps) != 0) { uya_log("movie at LSN %u: disc read failed", lsn); free(ps); return 0; } + es = malloc((size_t)sectors * 2048); + size_t n = pss_extract_video(ps, (size_t)sectors * 2048, es, (size_t)sectors * 2048); + int16_t *pcm = 0; + size_t pcm_frames = 0; + int rate = 0; + { + uint8_t *au = malloc((size_t)sectors * 2048); + size_t an = au ? pss_extract_audio(ps, (size_t)sectors * 2048, track, au, (size_t)sectors * 2048) : 0; + if (!an && track && au) an = pss_extract_audio(ps, (size_t)sectors * 2048, 0, au, (size_t)sectors * 2048); + if (an) pcm_frames = pss_decode_audio(au, an, &pcm, &rate); + free(au); + } + free(ps); + plm_video_t *v = plm_video_create_with_buffer(plm_buffer_create_with_memory(es, n, 0), 1); + int w = plm_video_get_width(v), h = plm_video_get_height(v); + double fps = plm_video_get_framerate(v); + if (w <= 0 || fps <= 0) { uya_log("movie at LSN %u: no video", lsn); plm_video_destroy(v); free(es); return 0; } + uya_log("movie at LSN %u: %ux%u at %.2f fps, %u KB; sound track %d: %s%.1f s at %d Hz", lsn, w, h, fps, (unsigned)(n / 1024), track, + pcm_frames ? "" : "none ", pcm_frames && rate ? (double)pcm_frames / rate : 0.0, rate); + uint32_t *px = malloc((size_t)w * h * 4); + char tag[32]; + snprintf(tag, sizeof tag, "movie_%u", lsn); + if (pcm_frames) plat_audio_play(pcm, pcm_frames, rate, tag); + plat_movie_started(); + uint64_t t0 = plat_time_us(); + int nowait = getenv("UYA_NOWAIT") != 0, dropped = 0; + plm_frame_t *f; + while ((f = plm_video_decode(v))) { + uint64_t due = t0 + (uint64_t)(frames * 1000000.0 / fps), now = plat_time_us(); + int late = !nowait && now > due + (uint64_t)(1000000.0 / fps); /* a frame behind the sound: not shown, to catch up */ + if (due > now && !nowait) plat_sleep_us(due - now); + if (!late) { + plm_frame_to_bgra(f, (uint8_t *)px, w * 4); + { /* GL mode: the GS thread draws it into the window */ + extern int uya_gs_movie_frame(const uint32_t *px, int w, int h); + int shown = uya_gs_movie_frame(px, w, h); + if (!plat_present(shown ? 0 : px, w, h)) { uya_log("window closed"); exit(0); } + } + } else dropped++; + frames++; + if (plat_key_skip()) { skipped = 1; break; } + } + plat_audio_stop(); + free(pcm); + uya_log("movie at LSN %u: %d frames shown%s (%d dropped to keep up with the sound)", lsn, frames - dropped, skipped ? " (skipped)" : "", dropped); + free(px); + plm_video_destroy(v); + free(es); + return skipped; +} diff --git a/games/rac1/host/src/movie/pss.h b/games/rac1/host/src/movie/pss.h new file mode 100644 index 0000000..070d11e --- /dev/null +++ b/games/rac1/host/src/movie/pss.h @@ -0,0 +1,111 @@ +/* pss.h: MPEG-2 program stream (PS2 .pss) demuxer: pulls the video elementary + * stream (stream id 0xE0) out of pack/PES framing. */ +#ifndef UYA_PSS_H +#define UYA_PSS_H +#include +#include + +/* Appends the payload of every video PES packet in `ps` to `out` (capacity + * `cap`); returns the number of bytes written. */ +static size_t pss_extract_video(const uint8_t *ps, size_t n, uint8_t *out, size_t cap) +{ + size_t i = 0, w = 0; + while (i + 4 <= n) { + if (ps[i] != 0 || ps[i + 1] != 0 || ps[i + 2] != 1) { i++; continue; } + uint8_t id = ps[i + 3]; + if (id == 0xBA) { /* pack header */ + if ((ps[i + 4] & 0xC0) == 0x40) i += 14 + (ps[i + 13] & 7); /* MPEG-2 */ + else i += 12; /* MPEG-1 */ + continue; + } + if (id == 0xB9) { i += 4; continue; } /* program end */ + if (id < 0xBB) { i++; continue; } /* not a PES/system packet */ + if (i + 6 > n) break; + size_t len = ((size_t)ps[i + 4] << 8) | ps[i + 5]; + size_t end = i + 6 + len; + if (end > n) end = n; + if (id == 0xE0 && (ps[i + 6] & 0xC0) == 0x80) { /* MPEG-2 PES header */ + size_t p = i + 9 + ps[i + 8]; + if (p < end && w + (end - p) <= cap) { memcpy(out + w, ps + p, end - p); w += end - p; } + } + i = end; + } + return w; +} + +/* Audio: private stream 1 (0xBD). Each payload starts with a 4-byte substream + * header (ff a1 00 0N, N = the track; the movies carry several language + * tracks). The track data is an "SShd" header then "SSbd" and PS2 ADPCM. + * Appends track `track`'s bytes to `out`; returns the count. */ +static size_t pss_extract_audio(const uint8_t *ps, size_t n, int track, uint8_t *out, size_t cap) +{ + size_t i = 0, w = 0; + while (i + 4 <= n) { + if (ps[i] != 0 || ps[i + 1] != 0 || ps[i + 2] != 1) { i++; continue; } + uint8_t id = ps[i + 3]; + if (id == 0xBA) { i += ((ps[i + 4] & 0xC0) == 0x40) ? 14 + (ps[i + 13] & 7) : 12; continue; } + if (id == 0xB9) { i += 4; continue; } + if (id < 0xBB) { i++; continue; } + if (i + 6 > n) break; + size_t len = ((size_t)ps[i + 4] << 8) | ps[i + 5]; + size_t end = i + 6 + len; + if (end > n) end = n; + if (id == 0xBD && (ps[i + 6] & 0xC0) == 0x80) { + size_t p = i + 9 + ps[i + 8]; + if (p + 4 <= end && ps[p] == 0xFF && ps[p + 3] == track) { + p += 4; + if (w + (end - p) <= cap) { memcpy(out + w, ps + p, end - p); w += end - p; } + } + } + i = end; + } + return w; +} + +static uint32_t pss_le32(const uint8_t *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((uint32_t)p[3] << 24); } + +/* Decodes an SShd/SSbd track (PS2 ADPCM, channels interleaved in blocks of + * `interleave` bytes) into interleaved 16-bit stereo. Returns frames, or 0. */ +static size_t pss_decode_audio(const uint8_t *a, size_t n, int16_t **out, int *rate) +{ + static const int fk[5][2] = {{0, 0}, {60, 0}, {115, -52}, {98, -55}, {122, -60}}; + if (n < 0x28 || memcmp(a, "SShd", 4) || pss_le32(a + 8) != 0x10) return 0; + uint32_t hsz = pss_le32(a + 4), ch = pss_le32(a + 16), il = pss_le32(a + 20); + *rate = (int)pss_le32(a + 12); + const uint8_t *b = a + 8 + hsz; + if (b + 8 > a + n || memcmp(b, "SSbd", 4) || ch < 1 || ch > 2 || il < 16 || il % 16) return 0; + size_t len = pss_le32(b + 4); + b += 8; + if (len > (size_t)(a + n - b)) len = (size_t)(a + n - b); + size_t blocks = len / (il * ch), per = il / 16 * 28; + size_t frames = blocks * per; + int16_t *o = (int16_t *)malloc(frames * 4 + 4); + if (!o) return 0; + int hist[2][2] = {{0, 0}, {0, 0}}; + for (size_t k = 0; k < blocks; k++) + for (uint32_t c = 0; c < ch; c++) { + const uint8_t *blk = b + (k * ch + c) * il; + for (uint32_t f = 0; f < il / 16; f++) { + const uint8_t *fr = blk + f * 16; + int sh = fr[0] & 15, flt = (fr[0] >> 4) & 7; + if (flt > 4) flt = 0; + if (sh > 12) sh = 9; + for (int s = 0; s < 28; s++) { + int nib = (fr[2 + s / 2] >> ((s & 1) * 4)) & 15; + if (nib >= 8) nib -= 16; + int v = (nib << 12) >> sh; + v += (hist[c][0] * fk[flt][0] + hist[c][1] * fk[flt][1] + 32) >> 6; + if (v > 32767) v = 32767; + if (v < -32768) v = -32768; + hist[c][1] = hist[c][0]; + hist[c][0] = v; + size_t idx = (k * per + f * 28 + s) * 2; + o[idx + c] = (int16_t)v; + if (ch == 1) o[idx + 1] = (int16_t)v; + } + } + } + *out = o; + return frames; +} +#endif diff --git a/games/rac1/host/src/platform_linux.c b/games/rac1/host/src/platform_linux.c new file mode 100644 index 0000000..df8d80e --- /dev/null +++ b/games/rac1/host/src/platform_linux.c @@ -0,0 +1,280 @@ +/* Linux (i386) platform layer: fixed mappings and the int3 trap for calls + * into PS2 code the host does not implement. */ +#ifdef __linux__ +#define _GNU_SOURCE +#include "uya_runtime.h" +#include +#include +#include +#include +#include +#include +#include + +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0x100000 +#endif + +int plat_map_fixed(uint32_t addr, uint32_t size, int exec) +{ + int prot = PROT_READ | PROT_WRITE | (exec ? PROT_EXEC : 0); + void *p = mmap((void *)(uintptr_t)addr, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (p == MAP_FAILED || p != (void *)(uintptr_t)addr) { + uya_log("cannot map 0x%08X+0x%X (%p)", addr, size, p); + return -1; + } + return 0; +} + +void plat_flush_icache(void *p, size_t n) { (void)p; (void)n; /* x86 keeps I/D coherent */ } + +const char *plat_exe_dir(void) +{ + static char d[1024]; + if (!d[0]) { + ssize_t n = readlink("/proc/self/exe", d, sizeof d - 1); + if (n <= 0) { strcpy(d, "."); return d; } + d[n] = 0; + char *s = strrchr(d, '/'); + if (s) *s = 0; + } + return d; +} + +void plat_at_exit(void) {} + +#include +uint64_t plat_time_us(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (uint64_t)t.tv_sec * 1000000u + t.tv_nsec / 1000; +} + +void plat_sleep_us(uint64_t us) +{ + struct timespec t = { (time_t)(us / 1000000), (long)(us % 1000000) * 1000 }; + nanosleep(&t, 0); +} + +/* A call landed on an int3 (0xCC) byte in PS2 code space: log it, then return + * to the caller as if the function returned 0. cdecl: the caller pops the + * arguments, so popping the return address is all that is needed. */ +static void on_trap(int sig, siginfo_t *si, void *ctxv) +{ + ucontext_t *uc = ctxv; + greg_t *g = uc->uc_mcontext.gregs; + uint32_t eip = (uint32_t)g[REG_EIP]; + uint32_t esp = (uint32_t)g[REG_ESP]; + uint32_t at = eip - 1; /* int3 already advanced eip */ + (void)si; + if (sig != SIGTRAP || at < PS2_RAM_LO || at >= PS2_RAM_HI) { + uya_log("fatal signal %d at eip=0x%08X", sig, eip); + uya_report(); + _exit(1); + } + uint32_t ret = *(uint32_t *)(uintptr_t)esp; + uya_trap_call(at, ret, (const uint32_t *)(uintptr_t)(esp + 4)); + g[REG_EAX] = 0; + g[REG_EDX] = 0; + g[REG_EIP] = ret; + g[REG_ESP] = esp + 4; +} + +static void on_fault(int sig, siginfo_t *si, void *ctxv) +{ + ucontext_t *uc = ctxv; + uya_log("crash: signal %d, fault address %p, eip=0x%08X", sig, si->si_addr, (uint32_t)uc->uc_mcontext.gregs[REG_EIP]); + uya_report(); + _exit(1); +} + +/* testing: UYA_PROF=file samples the program counter of whichever thread is + * running every millisecond of CPU time and writes "eip tid count" lines at + * exit (symbolize with nm; each thread's own pc, no call stacks) */ +#include +#include +#define PROF_N (1 << 16) +static struct { uint32_t eip, tid, n; } prof_tab[PROF_N]; +static const char *prof_path; +static void on_prof(int sig, siginfo_t *si, void *ctxv) +{ + ucontext_t *uc = ctxv; + uint32_t eip = (uint32_t)uc->uc_mcontext.gregs[REG_EIP], tid = (uint32_t)syscall(SYS_gettid); + uint32_t h = (eip * 2654435761u ^ tid * 40503u) >> 16; + (void)sig; (void)si; + for (int k = 0; k < 64; k++) { + uint32_t j = (h + k) & (PROF_N - 1); + if (prof_tab[j].n && (prof_tab[j].eip != eip || prof_tab[j].tid != tid)) continue; + prof_tab[j].eip = eip; prof_tab[j].tid = tid; __atomic_add_fetch(&prof_tab[j].n, 1, __ATOMIC_RELAXED); + return; + } +} +static void prof_dump(void) +{ + FILE *f = fopen(prof_path, "w"); + if (!f) return; + for (int j = 0; j < PROF_N; j++) if (prof_tab[j].n) fprintf(f, "%08x %u %u\n", prof_tab[j].eip, prof_tab[j].tid, prof_tab[j].n); + fclose(f); +} +static void prof_init(void) +{ + prof_path = getenv("UYA_PROF"); + if (!prof_path) return; + struct sigaction sa; + memset(&sa, 0, sizeof sa); + sa.sa_flags = SA_SIGINFO | SA_RESTART; + sa.sa_sigaction = on_prof; + sigaction(SIGPROF, &sa, 0); + struct itimerval it = { { 0, 1000 }, { 0, 1000 } }; + setitimer(ITIMER_PROF, &it, 0); + atexit(prof_dump); +} + +int plat_install_trap_handler(void) +{ + prof_init(); + struct sigaction sa; + static char altstack[64 * 1024]; + stack_t ss = { .ss_sp = altstack, .ss_size = sizeof altstack, .ss_flags = 0 }; + sigaltstack(&ss, 0); + memset(&sa, 0, sizeof sa); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = on_trap; + sigaction(SIGTRAP, &sa, 0); + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sa.sa_sigaction = on_fault; + sigaction(SIGSEGV, &sa, 0); + sigaction(SIGBUS, &sa, 0); + sigaction(SIGILL, &sa, 0); + sigaction(SIGFPE, &sa, 0); + return 0; +} +#endif + +#ifdef __linux__ +/* No window on Linux (yet): with UYA_DUMP=dir every 15th presented frame is + * written there as a PPM, so output can be checked. */ +#include +int plat_present(const uint32_t *px, int w, int h) +{ + static unsigned long n; + const char *dir = getenv("UYA_DUMP"); + static long every = -1; + if (every < 0) { const char *e = getenv("UYA_DUMP_EVERY"); every = e ? atol(e) : 15; if (every < 1) every = 1; } + if (!px) return 1; /* no new frame this vsync */ + static long from = -1; + if (from < 0) { const char *e = getenv("UYA_DUMP_FROM"); from = e ? atol(e) : 0; } /* testing: first vsync to dump */ + if (dir && (n % every) == 0 && uya_frame_count() >= (uint32_t)from) { + char path[512]; + snprintf(path, sizeof path, "%s/frame_%05lu.ppm", dir, n); + FILE *f = fopen(path, "wb"); + if (f) { + fprintf(f, "P6 %d %d 255\n", w, h); + for (int i = 0; i < w * h; i++) { uint32_t c = px[i]; fputc((c >> 16) & 0xFF, f); fputc((c >> 8) & 0xFF, f); fputc(c & 0xFF, f); /* BGRX in memory */ } + fclose(f); + } + } + n++; + return 1; +} +/* Scripted input for testing: UYA_KEYS="start@600+10,cross@900" presses a + * button from vsync 600 for 10 vsyncs (default 6). During a movie the vsync + * count stands still, so a press that is due skips the movie. */ +static const struct { const char *name; unsigned bit; } pad_names[] = { + {"select", 0x0001}, {"start", 0x0008}, {"up", 0x0010}, {"right", 0x0020}, {"down", 0x0040}, {"left", 0x0080}, + {"l2", 0x0100}, {"r2", 0x0200}, {"l1", 0x0400}, {"r1", 0x0800}, + {"triangle", 0x1000}, {"circle", 0x2000}, {"cross", 0x4000}, {"square", 0x8000}, + /* the left stick pushed all the way (testing; libpad2's scePad2Read reports it) */ + {"lup", 0x10000}, {"ldown", 0x20000}, {"lleft", 0x40000}, {"lright", 0x80000}, + {"rup", 0x100000}, {"rdown", 0x200000}, {"rleft", 0x400000}, {"rright", 0x800000}, +}; +int plat_pad_sticks(uint8_t st[4]) { (void)st; return 0; } +unsigned plat_pad_buttons(void) +{ + const char *s = getenv("UYA_KEYS"); + unsigned now = uya_frame_count(), bits = 0; + while (s && *s) { + char name[16]; + unsigned at = 0, dur = 6; + int k = 0; + while (*s && *s != '@' && *s != ',' && k < 15) name[k++] = *s++; + name[k] = 0; + if (*s == '@') at = (unsigned)strtoul(s + 1, (char **)&s, 10); + if (*s == '+') dur = (unsigned)strtoul(s + 1, (char **)&s, 10); + if (now >= at && now < at + dur) + for (unsigned i = 0; i < sizeof pad_names / sizeof pad_names[0]; i++) + if (!strcmp(name, pad_names[i].name)) bits |= pad_names[i].bit; + while (*s && *s != ',') s++; + if (*s == ',') s++; + } + return bits; +} +/* UYA_SKIPMOVIES=n skips the first n movies (plat_movie_started counts them) */ +static int movies_started; +void plat_movie_started(void) { movies_started++; } +int plat_key_skip(void) +{ + const char *e = getenv("UYA_SKIPMOVIES"); + if (e && movies_started <= atoi(e)) return 1; + return (plat_pad_buttons() & 0x4008) != 0; +} +void plat_poll(void) {} + +/* Sound on Linux: with UYA_DUMP set, each sound is saved there as a WAV file. */ +void plat_audio_play(const int16_t *lr, size_t frames, int rate, const char *tag) +{ + const char *dir = getenv("UYA_DUMP"); + if (!dir) return; + char path[512]; + snprintf(path, sizeof path, "%s/%s.wav", dir, tag); + FILE *f = fopen(path, "wb"); + if (!f) return; + uint32_t data = (uint32_t)(frames * 4), v; + fwrite("RIFF", 1, 4, f); v = 36 + data; fwrite(&v, 4, 1, f); + fwrite("WAVEfmt ", 1, 8, f); v = 16; fwrite(&v, 4, 1, f); + uint16_t pcm = 1, ch = 2, ba = 4, bits = 16; + fwrite(&pcm, 2, 1, f); fwrite(&ch, 2, 1, f); + v = (uint32_t)rate; fwrite(&v, 4, 1, f); v = (uint32_t)rate * 4; fwrite(&v, 4, 1, f); + fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&data, 4, 1, f); + fwrite(lr, 4, frames, f); + fclose(f); +} +void plat_audio_stop(void) {} +#endif + +#ifdef __linux__ +/* Sound effects on Linux: with UYA_DUMP set they go to sfx_48k_stereo.raw + * (16-bit PCM). Playback is simulated with the clock so the mixer is paced. */ +static uint64_t stream_written, stream_t0; +uint64_t uya_fixed_clock(void); +static uint64_t stream_now(void) { uint64_t v = uya_fixed_clock(); return v ? v : plat_time_us(); } /* UYA_FIXEDTIME: the game's clock */ +void plat_stream_write(const int16_t *lr, int frames) +{ + static FILE *f; + static int tried; + if (!tried) { + const char *dir = getenv("UYA_DUMP"); + tried = 1; + if (dir) { char path[512]; snprintf(path, sizeof path, "%s/sfx_48k_stereo.raw", dir); f = fopen(path, "wb"); } + } + if (!stream_t0) stream_t0 = stream_now(); + if (f) fwrite(lr, 4, (size_t)frames, f); + stream_written += (uint64_t)frames; +} +int plat_stream_queued(void) +{ + if (!stream_t0) return 0; + uint64_t played = (stream_now() - stream_t0) * 48000 / 1000000; + return played >= stream_written ? 0 : (int)(stream_written - played); +} +/* OpenGL: this build is 32-bit static (musl) and cannot load a GL driver, so + * the GS draws in software (the GL renderer is tested with tools/gs_replay.c) */ +int plat_gl_active; +int plat_gl_init(void) { return 0; } +void plat_gl_current(int on) { (void)on; } +void plat_gl_swap(void) {} +void plat_gl_size(int *w, int *h) { *w = 640; *h = 480; } +void *plat_gl_proc(const char *name) { (void)name; return 0; } +#endif diff --git a/games/rac1/host/src/platform_win32.c b/games/rac1/host/src/platform_win32.c new file mode 100644 index 0000000..ea49e89 --- /dev/null +++ b/games/rac1/host/src/platform_win32.c @@ -0,0 +1,739 @@ +/* Windows (x86) platform layer. The menu code is in rac1_game.dll; the + * launcher (launcher/launcher_win32.c) is a tiny exe whose own image covers + * PS2 RAM, so that range is reserved before Windows places the stack and heaps. */ +#ifdef _WIN32 +#include "uya_runtime.h" +#include +#include +#include +#include +#include +#include +#include + +extern int plat_gl_active; /* the GS presents with OpenGL (below) */ + +int plat_map_fixed(uint32_t addr, uint32_t size, int exec) +{ + DWORD prot = exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; + void *p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_RESERVE | MEM_COMMIT, prot); + if (p == (void *)(uintptr_t)addr) return 0; + /* already there? (PS2 RAM comes from the launcher's image section) */ + MEMORY_BASIC_INFORMATION mi; + uint32_t a = addr; + while (a < addr + size) { + if (!VirtualQuery((void *)(uintptr_t)a, &mi, sizeof mi) || mi.State != MEM_COMMIT) { + uya_log("cannot map 0x%08X+0x%X (0x%08X is in use)", addr, size, a); + return -1; + } + DWORD old; + VirtualProtect(mi.BaseAddress, mi.RegionSize, prot, &old); + a = (uint32_t)(uintptr_t)mi.BaseAddress + (uint32_t)mi.RegionSize; + } + return 0; +} + +void plat_flush_icache(void *p, size_t n) { FlushInstructionCache(GetCurrentProcess(), p, n); } + +const char *plat_exe_dir(void) +{ + static char d[MAX_PATH]; + if (!d[0]) { + DWORD n = GetModuleFileNameA(0, d, sizeof d); + if (!n) { strcpy(d, "."); return d; } + char *s = strrchr(d, '\\'); + if (s) *s = 0; + } + return d; +} + +/* Started by double-click, the console belongs to this process alone and + * would close at once; wait for a key so the log can be read. */ +void plat_at_exit(void) +{ + static int done; + DWORD pids[2]; + if (done++) return; + if (GetConsoleProcessList(pids, 2) == 1) { + fprintf(stderr, "\nPress Enter to close this window.\n"); + getchar(); + } +} + +static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep) +{ + EXCEPTION_RECORD *er = ep->ExceptionRecord; + CONTEXT *c = ep->ContextRecord; + uint32_t at = (uint32_t)(uintptr_t)er->ExceptionAddress; + if (er->ExceptionCode == EXCEPTION_BREAKPOINT && at >= PS2_RAM_LO && at < PS2_RAM_HI) { + uint32_t ret = *(uint32_t *)(uintptr_t)c->Esp; + uya_trap_call(at, ret, (const uint32_t *)(uintptr_t)(c->Esp + 4)); + c->Eax = 0; c->Edx = 0; + c->Eip = ret; c->Esp += 4; + return EXCEPTION_CONTINUE_EXECUTION; + } + uya_log("crash: exception 0x%08lX at 0x%08X, fault address 0x%08lX", er->ExceptionCode, at, + er->NumberParameters > 1 ? (unsigned long)er->ExceptionInformation[1] : 0ul); + uya_log("crash: thread %lu, eax %08lX ecx %08lX edx %08lX ebx %08lX esi %08lX edi %08lX esp %08lX ebp %08lX", + GetCurrentThreadId(), c->Eax, c->Ecx, c->Edx, c->Ebx, c->Esi, c->Edi, c->Esp, c->Ebp); + { /* the stack: return addresses near its top, then the frame chain */ + const uint32_t *sp = (const uint32_t *)(uintptr_t)c->Esp; + for (int i = 0; i < 24; i += 8) + uya_log("crash: stack +%02X: %08X %08X %08X %08X %08X %08X %08X %08X", i * 4, sp[i], sp[i + 1], sp[i + 2], sp[i + 3], sp[i + 4], sp[i + 5], sp[i + 6], sp[i + 7]); + uint32_t bp = (uint32_t)c->Ebp; + for (int i = 0; i < 12 && bp > 0x10000 && !IsBadReadPtr((void *)(uintptr_t)bp, 8); i++) { + uya_log("crash: frame %d: return %08X", i, ((uint32_t *)(uintptr_t)bp)[1]); + bp = ((uint32_t *)(uintptr_t)bp)[0]; + } + } + uya_report(); + plat_at_exit(); + ExitProcess(1); + return EXCEPTION_CONTINUE_SEARCH; +} + +int plat_install_trap_handler(void) +{ + return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1; +} + +uint64_t plat_time_us(void) +{ + static LARGE_INTEGER f; + LARGE_INTEGER c; + if (!f.QuadPart) QueryPerformanceFrequency(&f); + QueryPerformanceCounter(&c); + return (uint64_t)(c.QuadPart / f.QuadPart) * 1000000u + (uint64_t)(c.QuadPart % f.QuadPart) * 1000000u / f.QuadPart; +} + +void plat_sleep_us(uint64_t us) +{ + static int period; + if (!period) { timeBeginPeriod(1); period = 1; } /* 1 ms sleep resolution */ + if (us >= 1000) Sleep((DWORD)(us / 1000)); +} + +/* ---- the window ---- + * A plain Win32 window; frames are stretched into it with StretchDIBits + * (4:3, 640x480 to start, resizable). */ +static HWND win; +static volatile int win_closed; + +/* Controller only: the keyboard does not play (F9 still records a glitch capture). */ + +/* XInput gamepads (loaded at run time: no XInput, no gamepad) */ +typedef struct { WORD wButtons; BYTE bLeftTrigger, bRightTrigger; SHORT sThumbLX, sThumbLY, sThumbRX, sThumbRY; } xi_pad_t; +typedef struct { DWORD dwPacketNumber; xi_pad_t Gamepad; } xi_state_t; +typedef DWORD (WINAPI *xi_get_t)(DWORD, xi_state_t *); +static xi_get_t xi_get; +static int xi_tried; +static int xi_read(xi_state_t *st) +{ + if (!xi_tried) { + static const char *dlls[] = { "xinput1_4.dll", "xinput1_3.dll", "xinput9_1_0.dll" }; + xi_tried = 1; + for (int i = 0; i < 3 && !xi_get; i++) { HMODULE m = LoadLibraryA(dlls[i]); if (m) xi_get = (xi_get_t)(void *)GetProcAddress(m, "XInputGetState"); } + if (xi_get) uya_log("gamepad: XInput available"); + } + if (!xi_get) return 0; + static uint64_t next_try; /* a missing pad is slow to poll: try it again every second */ + uint64_t t = plat_time_us(); + if (next_try && t < next_try) return 0; + if (xi_get(0, st) != 0) { next_try = t + 1000000; return 0; } + next_try = 0; + return 1; +} +static unsigned xi_buttons(const xi_pad_t *g) +{ + static const struct { WORD xi; unsigned ps2; } map[] = { + {0x0001, 0x0010}, {0x0002, 0x0040}, {0x0004, 0x0080}, {0x0008, 0x0020}, /* d-pad up down left right */ + {0x0010, 0x0008}, {0x0020, 0x0001}, /* Start, Back = Select */ + {0x0040, 0x0002}, {0x0080, 0x0004}, /* L3, R3 */ + {0x0100, 0x0400}, {0x0200, 0x0800}, /* LB = L1, RB = R1 */ + {0x1000, 0x4000}, {0x2000, 0x2000}, {0x4000, 0x8000}, {0x8000, 0x1000}, /* A = Cross, B = Circle, X = Square, Y = Triangle */ + }; + unsigned b = 0; + for (unsigned i = 0; i < sizeof map / sizeof map[0]; i++) if (g->wButtons & map[i].xi) b |= map[i].ps2; + if (g->bLeftTrigger > 64) b |= 0x0100; /* L2 */ + if (g->bRightTrigger > 64) b |= 0x0200; /* R2 */ + return b; +} +static uint8_t stick(SHORT v, int invert) +{ + int x = v / 256; /* -128..127 */ + if (x > -24 && x < 24) return 0x7F; /* dead zone */ + if (invert) x = -x - 1; + x += 0x80; + return (uint8_t)(x < 0 ? 0 : x > 255 ? 255 : x); +} +/* ---- PlayStation controllers (DualShock 4, DualSense) over USB or Bluetooth ---- + * Read as plain HID devices (hid.dll and setupapi.dll loaded at run time) on + * a thread of their own, which keeps the latest state. Reports handled: + * DualShock 4: 0x01 (USB, and Bluetooth before the full mode) and 0x11 + * (Bluetooth full mode): LX LY RX RY, hat + face buttons, shoulder and + * stick buttons, PS, L2 R2 analog, from offset 1 (0x01) or 3 (0x11) + * DualSense: 0x01 over USB (64 bytes) and 0x31 over Bluetooth: LX LY RX RY + * L2 R2, a counter, then the button bytes as the DualShock 4 has them; + * its short Bluetooth 0x01 report has the DualShock 4 layout */ +typedef struct { ULONG Size; USHORT VendorID, ProductID, VersionNumber; } hid_attr_t; /* HIDD_ATTRIBUTES */ +typedef struct { DWORD cbSize; GUID InterfaceClassGuid; DWORD Flags; ULONG_PTR Reserved; } sp_ifdata_t; +typedef struct { DWORD cbSize; CHAR DevicePath[1]; } sp_ifdetail_t; +static void (WINAPI *p_HidD_GetHidGuid)(GUID *); +static BOOLEAN (WINAPI *p_HidD_GetAttributes)(HANDLE, hid_attr_t *); +static HANDLE (WINAPI *p_SetupDiGetClassDevsA)(const GUID *, PCSTR, HWND, DWORD); +static BOOL (WINAPI *p_SetupDiEnumDeviceInterfaces)(HANDLE, void *, const GUID *, DWORD, sp_ifdata_t *); +static BOOL (WINAPI *p_SetupDiGetDeviceInterfaceDetailA)(HANDLE, sp_ifdata_t *, sp_ifdetail_t *, DWORD, DWORD *, void *); +static BOOL (WINAPI *p_SetupDiDestroyDeviceInfoList)(HANDLE); +static volatile LONG ds_ok; /* a controller is connected and reporting */ +static volatile unsigned ds_buttons; /* PS2 pad bits */ +static volatile uint8_t ds_sticks[4]; /* LX LY RX RY, 0x80 centre, down = +Y as on the PS2 */ + +static HANDLE ds_open(int *dualsense) +{ + GUID g; p_HidD_GetHidGuid(&g); + HANDLE set = p_SetupDiGetClassDevsA(&g, 0, 0, 0x02 | 0x10); /* DIGCF_PRESENT | DIGCF_DEVICEINTERFACE */ + if (set == INVALID_HANDLE_VALUE) return INVALID_HANDLE_VALUE; + HANDLE found = INVALID_HANDLE_VALUE; + static int logged; /* the first scan is logged: which HID devices there are, and why one was not used */ + int ndev = 0; + for (DWORD i = 0; found == INVALID_HANDLE_VALUE; i++) { + sp_ifdata_t ifd; memset(&ifd, 0, sizeof ifd); ifd.cbSize = sizeof ifd; + if (!p_SetupDiEnumDeviceInterfaces(set, 0, &g, i, &ifd)) break; + DWORD need = 0; + p_SetupDiGetDeviceInterfaceDetailA(set, &ifd, 0, 0, &need, 0); + if (!need) continue; + sp_ifdetail_t *det = malloc(need); + if (!det) continue; + det->cbSize = sizeof(DWORD) + sizeof(CHAR); /* 5 on 32-bit Windows */ + if (p_SetupDiGetDeviceInterfaceDetailA(set, &ifd, det, need, 0, 0)) { + ndev++; + { /* the path names the vendor and product: only Sony devices are opened */ + char low[512]; size_t k = 0; + for (; det->DevicePath[k] && k < sizeof low - 1; k++) low[k] = (char)tolower((unsigned char)det->DevicePath[k]); + low[k] = 0; + int sony = strstr(low, "054c") != 0; + if (!logged && sony) uya_log("gamepad: HID device %s", det->DevicePath); + if (!sony) { free(det); continue; } + } + HANDLE h = CreateFileA(det->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); + if (h == INVALID_HANDLE_VALUE) /* some systems open game controllers for reading only */ + h = CreateFileA(det->DevicePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); + if (h == INVALID_HANDLE_VALUE && !logged) uya_log("gamepad: cannot open it (error %lu)", GetLastError()); + if (h != INVALID_HANDLE_VALUE) { + hid_attr_t at; memset(&at, 0, sizeof at); at.Size = sizeof at; + int ok = 0; + if (p_HidD_GetAttributes(h, &at) && at.VendorID == 0x054C) { + if (at.ProductID == 0x05C4 || at.ProductID == 0x09CC || at.ProductID == 0x0BA0) { ok = 1; *dualsense = 0; } /* DualShock 4 (and its USB adapter) */ + if (at.ProductID == 0x0CE6 || at.ProductID == 0x0DF2) { ok = 1; *dualsense = 1; } /* DualSense (Edge) */ + } + if (!logged) uya_log("gamepad: 054C:%04X%s", at.ProductID, ok ? " (a DualShock 4 / DualSense)" : " (not a known controller)"); + if (ok) { found = h; uya_log("gamepad: %s (054C:%04X) found", *dualsense ? "DualSense" : "DualShock 4", at.ProductID); } + else CloseHandle(h); + } + } + free(det); + } + p_SetupDiDestroyDeviceInfoList(set); + if (!logged) uya_log("gamepad: %d HID devices, %s", ndev, found != INVALID_HANDLE_VALUE ? "a PlayStation controller is used" : "no PlayStation controller found (connected later, it is picked up)"); + logged = 1; + return found; +} + +/* PS2 bits from the two button bytes the DualShock 4 and DualSense share, and the PS byte */ +static unsigned ds_map(uint8_t b0, uint8_t b1) +{ + static const unsigned hat[9] = { 0x10, 0x10 | 0x20, 0x20, 0x20 | 0x40, 0x40, 0x40 | 0x80, 0x80, 0x80 | 0x10, 0 }; + unsigned b = hat[(b0 & 15) > 8 ? 8 : (b0 & 15)]; + if (b0 & 0x10) b |= 0x8000; /* Square */ + if (b0 & 0x20) b |= 0x4000; /* Cross */ + if (b0 & 0x40) b |= 0x2000; /* Circle */ + if (b0 & 0x80) b |= 0x1000; /* Triangle */ + if (b1 & 0x01) b |= 0x0400; /* L1 */ + if (b1 & 0x02) b |= 0x0800; /* R1 */ + if (b1 & 0x04) b |= 0x0100; /* L2 */ + if (b1 & 0x08) b |= 0x0200; /* R2 */ + if (b1 & 0x10) b |= 0x0001; /* Share / Create = Select */ + if (b1 & 0x20) b |= 0x0008; /* Options = Start */ + if (b1 & 0x40) b |= 0x0002; /* L3 */ + if (b1 & 0x80) b |= 0x0004; /* R3 */ + return b; +} + +static int ds_parse(const uint8_t *r, DWORD n, int dualsense) +{ + const uint8_t *s; /* the sticks */ + uint8_t b0, b1; + if (n < 10) return 0; + if (r[0] == 0x11 && n >= 12) { s = r + 3; b0 = s[4]; b1 = s[5]; } /* DualShock 4, Bluetooth full */ + else if (r[0] == 0x31 && n >= 13) { s = r + 2; b0 = s[7]; b1 = s[8]; } /* DualSense, Bluetooth full */ + else if (r[0] == 0x01 && dualsense && n >= 12 && n > 20) { s = r + 1; b0 = s[7]; b1 = s[8]; } /* DualSense, USB */ + else if (r[0] == 0x01) { s = r + 1; b0 = s[4]; b1 = s[5]; } /* DualShock 4; DualSense short Bluetooth */ + else return 0; + for (int i = 0; i < 4; i++) ds_sticks[i] = s[i]; + ds_buttons = ds_map(b0, b1); + return 1; +} + +static DWORD WINAPI ds_thread(LPVOID a) +{ + (void)a; + static uint8_t buf[1024]; + for (;;) { + int dualsense = 0; + HANDLE h = ds_open(&dualsense); + if (h == INVALID_HANDLE_VALUE) { Sleep(2000); continue; } + { /* asking a DualShock 4 on Bluetooth for its calibration switches it to the full reports (0x11) */ + static BOOLEAN (WINAPI *getf)(HANDLE, void *, ULONG); + if (!getf) { HMODULE m = GetModuleHandleA("hid.dll"); if (m) getf = (void *)GetProcAddress(m, "HidD_GetFeature"); } + if (getf && !dualsense) { uint8_t f[64]; memset(f, 0, sizeof f); f[0] = 0x05; getf(h, f, sizeof f); } + } + DWORD rlen = sizeof buf; /* one report per read (a longer buffer gets several, oldest first) */ + { + HMODULE m = GetModuleHandleA("hid.dll"); + BOOLEAN (WINAPI *gp)(HANDLE, void **) = m ? (void *)GetProcAddress(m, "HidD_GetPreparsedData") : 0; + BOOLEAN (WINAPI *fp)(void *) = m ? (void *)GetProcAddress(m, "HidD_FreePreparsedData") : 0; + LONG (WINAPI *caps)(void *, void *) = m ? (void *)GetProcAddress(m, "HidP_GetCaps") : 0; + void *pd = 0; + if (gp && fp && caps && gp(h, &pd)) { + USHORT c[32]; memset(c, 0, sizeof c); + if (caps(pd, c) == 0x00110000 && c[2] && c[2] <= sizeof buf) rlen = c[2]; /* HIDP_STATUS_SUCCESS; InputReportByteLength */ + fp(pd); + } + uya_log("gamepad: reports of %lu bytes", rlen); + } + int fails = 0; + for (;;) { + DWORD n = 0; + if (!ReadFile(h, buf, rlen, &n, 0)) { uya_log("gamepad: read failed (error %lu)", GetLastError()); if (++fails > 3) break; Sleep(5); continue; } + fails = 0; + { static int nlog; + if (nlog < 3) { nlog++; uya_log("gamepad: report %02X, %lu bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", buf[0], n, buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10]); } + } + if (ds_parse(buf, n, dualsense)) InterlockedExchange(&ds_ok, 1); + } + InterlockedExchange(&ds_ok, 0); + ds_buttons = 0; + CloseHandle(h); + uya_log("gamepad: PlayStation controller disconnected"); + Sleep(1000); + } + return 0; +} + +static void ds_start(void) +{ + static int tried; + if (tried) return; + tried = 1; + HMODULE hid = LoadLibraryA("hid.dll"), sa = LoadLibraryA("setupapi.dll"); + if (!hid || !sa) return; + p_HidD_GetHidGuid = (void *)GetProcAddress(hid, "HidD_GetHidGuid"); + p_HidD_GetAttributes = (void *)GetProcAddress(hid, "HidD_GetAttributes"); + p_SetupDiGetClassDevsA = (void *)GetProcAddress(sa, "SetupDiGetClassDevsA"); + p_SetupDiEnumDeviceInterfaces = (void *)GetProcAddress(sa, "SetupDiEnumDeviceInterfaces"); + p_SetupDiGetDeviceInterfaceDetailA = (void *)GetProcAddress(sa, "SetupDiGetDeviceInterfaceDetailA"); + p_SetupDiDestroyDeviceInfoList = (void *)GetProcAddress(sa, "SetupDiDestroyDeviceInfoList"); + if (!p_HidD_GetHidGuid || !p_HidD_GetAttributes || !p_SetupDiGetClassDevsA || !p_SetupDiEnumDeviceInterfaces || + !p_SetupDiGetDeviceInterfaceDetailA || !p_SetupDiDestroyDeviceInfoList) return; + HANDLE t = CreateThread(0, 0, ds_thread, 0, 0, 0); + if (t) CloseHandle(t); +} + +static uint8_t ds_axis(uint8_t v) /* a small dead zone; the game has its own on top */ +{ + return (v > 0x80 - 12 && v < 0x80 + 12) ? 0x7F : v; +} + +/* The same controllers through the Windows joystick API (winmm), when the + * HID reader does not get them: Windows itself reads a DualShock 4 or + * DualSense as a game controller, axes X Y = left stick, Z R = right stick, + * U V = L2 R2, buttons 1-14 Square Cross Circle Triangle L1 R1 L2 R2 + * Share Options L3 R3 PS touchpad, the d-pad as the POV hat. */ +static int jm_id = -1; +static uint64_t jm_next; +static int jm_read(JOYINFOEX *ji) +{ + uint64_t t = plat_time_us(); + if (jm_id < 0) { + if (t < jm_next) return 0; + jm_next = t + 2000000; /* looked for every 2 s */ + UINT n = joyGetNumDevs(); + for (UINT i = 0; i < n && i < 16; i++) { + JOYCAPSA cp; memset(&cp, 0, sizeof cp); + if (joyGetDevCapsA(i, &cp, sizeof cp) != JOYERR_NOERROR) continue; + JOYINFOEX tj; memset(&tj, 0, sizeof tj); tj.dwSize = sizeof tj; tj.dwFlags = JOY_RETURNALL; + if (joyGetPosEx(i, &tj) != JOYERR_NOERROR) continue; + uya_log("gamepad: joystick %u: %04X:%04X \"%s\", %u axes, %u buttons%s", i, cp.wMid, cp.wPid, cp.szPname, cp.wNumAxes, cp.wNumButtons, + cp.wMid == 0x054C ? " (a PlayStation controller: used)" : ""); + if (cp.wMid == 0x054C) { jm_id = (int)i; break; } + } + if (jm_id < 0) return 0; + } + memset(ji, 0, sizeof *ji); ji->dwSize = sizeof *ji; ji->dwFlags = JOY_RETURNALL; + if (joyGetPosEx((UINT)jm_id, ji) != JOYERR_NOERROR) { uya_log("gamepad: joystick %d gone", jm_id); jm_id = -1; jm_next = t + 2000000; return 0; } + return 1; +} +static uint8_t jm_axis(DWORD v) { return ds_axis((uint8_t)(v >> 8)); } /* 0..65535 -> 0..255, 0x80 centre */ +static unsigned jm_buttons(const JOYINFOEX *ji) +{ + static const unsigned map[14] = { 0x8000, 0x4000, 0x2000, 0x1000, 0x0400, 0x0800, 0x0100, 0x0200, 0x0001, 0x0008, 0x0002, 0x0004, 0, 0 }; + unsigned b = 0; + for (int i = 0; i < 14; i++) if (ji->dwButtons & (1u << i)) b |= map[i]; + DWORD pov = ji->dwPOV; + if (pov != 0xFFFF && pov <= 36000) { + int o = (int)((pov + 2250) / 4500) % 8; /* 0 up, 1 up-right, ... clockwise */ + static const unsigned dir[8] = { 0x10, 0x30, 0x20, 0x60, 0x40, 0xC0, 0x80, 0x90 }; + b |= dir[o]; + } + return b; +} + +int plat_pad_sticks(uint8_t st[4]) +{ + ds_start(); + if (ds_ok) { + st[0] = ds_axis(ds_sticks[0]); st[1] = ds_axis(ds_sticks[1]); + st[2] = ds_axis(ds_sticks[2]); st[3] = ds_axis(ds_sticks[3]); + return 1; + } + xi_state_t s; + if (xi_read(&s)) { + st[0] = stick(s.Gamepad.sThumbLX, 0); st[1] = stick(s.Gamepad.sThumbLY, 1); /* PS2: down is +Y */ + st[2] = stick(s.Gamepad.sThumbRX, 0); st[3] = stick(s.Gamepad.sThumbRY, 1); + return 1; + } + JOYINFOEX ji; + if (jm_read(&ji)) { + st[0] = jm_axis(ji.dwXpos); st[1] = jm_axis(ji.dwYpos); + st[2] = jm_axis(ji.dwZpos); st[3] = jm_axis(ji.dwRpos); + return 1; + } + return 0; +} + +/* testing: UYA_KEYS="start@600+10,cross@900" presses buttons from vsync 600 for 10 vsyncs (as on Linux) */ +static unsigned test_keys(void) +{ + static const struct { const char *name; unsigned bit; } nm[] = { + {"select", 0x0001}, {"start", 0x0008}, {"up", 0x0010}, {"right", 0x0020}, {"down", 0x0040}, {"left", 0x0080}, + {"l2", 0x0100}, {"r2", 0x0200}, {"l1", 0x0400}, {"r1", 0x0800}, + {"triangle", 0x1000}, {"circle", 0x2000}, {"cross", 0x4000}, {"square", 0x8000}, + {"lup", 0x10000}, {"ldown", 0x20000}, {"lleft", 0x40000}, {"lright", 0x80000}, + }; + static const char *keys; static int init; + if (!init) { init = 1; keys = getenv("UYA_KEYS"); } + const char *s = keys; + unsigned now = uya_frame_count(), bits = 0; + while (s && *s) { + char name[16]; unsigned at = 0, dur = 6; int k = 0; + while (*s && *s != '@' && *s != ',' && k < 15) name[k++] = *s++; + name[k] = 0; + if (*s == '@') at = (unsigned)strtoul(s + 1, (char **)&s, 10); + if (*s == '+') dur = (unsigned)strtoul(s + 1, (char **)&s, 10); + if (now >= at && now < at + dur) + for (unsigned i = 0; i < sizeof nm / sizeof nm[0]; i++) if (!strcmp(name, nm[i].name)) bits |= nm[i].bit; + while (*s && *s != ',') s++; + if (*s == ',') s++; + } + return bits; +} + +unsigned plat_pad_buttons(void) +{ + xi_state_t s; + JOYINFOEX ji; + unsigned b = test_keys(); + ds_start(); + if (ds_ok) b |= ds_buttons; + else if (xi_read(&s)) b |= xi_buttons(&s.Gamepad); + else if (jm_read(&ji)) b |= jm_buttons(&ji); + return b; +} + +static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + switch (m) { + case WM_CLOSE: win_closed = 1; DestroyWindow(h); return 0; + case WM_KEYDOWN: + if (w == VK_F9 && !(l & (1 << 30))) { void uya_gs_rec_hotkey(void); uya_gs_rec_hotkey(); return 0; } /* testing: record the GS input */ + return 0; + case WM_ERASEBKGND: + if (plat_gl_active) return 1; /* GL draws the whole window */ + break; + } + return DefWindowProcA(h, m, w, l); +} + +static void pump(void) +{ + MSG msg; + while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageA(&msg); } +} + +static void win_create(void) +{ + if (win) return; + WNDCLASSA wc = {0}; + RECT r = {0, 0, 640, 480}; + wc.style = CS_OWNDC; /* (OpenGL keeps the window's DC) */ + wc.lpfnWndProc = wnd_proc; + wc.hInstance = GetModuleHandleA(0); + wc.hCursor = LoadCursorA(0, (LPCSTR)IDC_ARROW); + wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + wc.lpszClassName = "UYAWrapper"; + RegisterClassA(&wc); + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); + win = CreateWindowA("UYAWrapper", "Project Solana", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, + r.right - r.left, r.bottom - r.top, 0, 0, wc.hInstance, 0); +} + +/* ---- OpenGL (the GS's GPU renderer, host/src/gs/gs_gl.c) ---- + * A 3.3 core context on the window through WGL; opengl32.dll is loaded at + * run time, so a machine without it just keeps the software renderer. */ +int plat_gl_active; +static HMODULE gl_lib; +static HDC gl_dc; +static HGLRC gl_rc; +static HGLRC (WINAPI *p_wglCreateContext)(HDC); +static BOOL (WINAPI *p_wglMakeCurrent)(HDC, HGLRC); +static BOOL (WINAPI *p_wglDeleteContext)(HGLRC); +static PROC (WINAPI *p_wglGetProcAddress)(LPCSTR); +void *plat_gl_proc(const char *name) +{ + void *p = p_wglGetProcAddress ? (void *)p_wglGetProcAddress(name) : 0; + if (p == (void *)1 || p == (void *)2 || p == (void *)3 || p == (void *)-1) p = 0; + if (!p && gl_lib) p = (void *)GetProcAddress(gl_lib, name); /* the GL 1.1 functions */ + return p; +} +int plat_gl_init(void) +{ + win_create(); + if (!win) return 0; + gl_lib = LoadLibraryA("opengl32.dll"); + if (!gl_lib) { uya_log("GL: no opengl32.dll"); return 0; } + p_wglCreateContext = (HGLRC (WINAPI *)(HDC))GetProcAddress(gl_lib, "wglCreateContext"); + p_wglMakeCurrent = (BOOL (WINAPI *)(HDC, HGLRC))GetProcAddress(gl_lib, "wglMakeCurrent"); + p_wglDeleteContext = (BOOL (WINAPI *)(HGLRC))GetProcAddress(gl_lib, "wglDeleteContext"); + p_wglGetProcAddress = (PROC (WINAPI *)(LPCSTR))GetProcAddress(gl_lib, "wglGetProcAddress"); + if (!p_wglCreateContext || !p_wglMakeCurrent || !p_wglDeleteContext || !p_wglGetProcAddress) return 0; + gl_dc = GetDC(win); + PIXELFORMATDESCRIPTOR pfd; + memset(&pfd, 0, sizeof pfd); + pfd.nSize = sizeof pfd; pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 32; pfd.iLayerType = PFD_MAIN_PLANE; + int pf = ChoosePixelFormat(gl_dc, &pfd); + if (!pf || !SetPixelFormat(gl_dc, pf, &pfd)) { uya_log("GL: no pixel format"); return 0; } + HGLRC tmp = p_wglCreateContext(gl_dc); + if (!tmp || !p_wglMakeCurrent(gl_dc, tmp)) { uya_log("GL: wglCreateContext failed"); return 0; } + HGLRC (WINAPI *create_attribs)(HDC, HGLRC, const int *) = (HGLRC (WINAPI *)(HDC, HGLRC, const int *))plat_gl_proc("wglCreateContextAttribsARB"); + if (!create_attribs) { uya_log("GL: no wglCreateContextAttribsARB (OpenGL 3.3 needed)"); p_wglMakeCurrent(0, 0); p_wglDeleteContext(tmp); return 0; } + const int attr[] = { 0x2091, 3, 0x2092, 3, 0x9126, 1, 0 }; /* 3.3 core */ + gl_rc = create_attribs(gl_dc, 0, attr); + p_wglMakeCurrent(0, 0); + p_wglDeleteContext(tmp); + if (!gl_rc || !p_wglMakeCurrent(gl_dc, gl_rc)) { uya_log("GL: no OpenGL 3.3 core context"); return 0; } + { /* UYA_VSYNC=1: swaps wait for the monitor (the game paces itself at 50 Hz anyway) */ + BOOL (WINAPI *swap_interval)(int) = (BOOL (WINAPI *)(int))plat_gl_proc("wglSwapIntervalEXT"); + const char *e = getenv("UYA_VSYNC"); + if (swap_interval) swap_interval(e ? atoi(e) : 0); + } + return 1; +} +void plat_gl_current(int on) { if (p_wglMakeCurrent) p_wglMakeCurrent(on ? gl_dc : 0, on ? gl_rc : 0); } +void plat_gl_swap(void) { SwapBuffers(gl_dc); } +void plat_gl_size(int *w, int *h) +{ + RECT rc; + if (win && GetClientRect(win, &rc)) { *w = rc.right; *h = rc.bottom; } + else { *w = 640; *h = 480; } +} + +int plat_present(const uint32_t *px, int w, int h) +{ + if (win_closed) return 0; + win_create(); + pump(); + if (win_closed) return 0; + if (plat_gl_active) { /* the GS thread draws the window; here only the frame rate in the title */ + extern unsigned long long nsends; + static uint64_t t0; static unsigned long long s0; static unsigned n0; + uint64_t t = plat_time_us(); + n0++; + if (!t0) { t0 = t; s0 = nsends; } + else if (t - t0 >= 1000000) { + double sec = (double)(t - t0) / 1e6; + char title[64]; + snprintf(title, sizeof title, "Project Solana - %.0f FPS", (double)(nsends - s0) / sec); + SetWindowTextA(win, title); + t0 = t; s0 = nsends; n0 = 0; + } + return 1; + } + { + BITMAPINFO bi = {0}; + RECT rc; + HDC dc = GetDC(win); + GetClientRect(win, &rc); + bi.bmiHeader.biSize = sizeof bi.bmiHeader; + bi.bmiHeader.biWidth = w; + bi.bmiHeader.biHeight = -h; /* top-down */ + bi.bmiHeader.biPlanes = 1; + bi.bmiHeader.biBitCount = 32; + bi.bmiHeader.biCompression = BI_RGB; + { + /* keep 4:3, black bars around */ + int cw = rc.right, ch = rc.bottom, vw = cw, vh = cw * 3 / 4; + if (vh > ch) { vh = ch; vw = ch * 4 / 3; } + SetStretchBltMode(dc, COLORONCOLOR); /* HALFTONE filters nicer but costs milliseconds a frame */ + if (px) StretchDIBits(dc, (cw - vw) / 2, (ch - vh) / 2, vw, vh, 0, 0, w, h, px, &bi, DIB_RGB_COLORS, SRCCOPY); + } + { /* debug: frame rate in the corner and the title (game frames drawn, and vsyncs: 50 = full speed) */ + extern unsigned long long nsends; + static uint64_t t0; static unsigned long long s0; static unsigned n0; + static char text[96] = ""; + uint64_t t = plat_time_us(); + n0++; + if (!t0) { t0 = t; s0 = nsends; } + else if (t - t0 >= 1000000) { + double sec = (double)(t - t0) / 1e6; + snprintf(text, sizeof text, "%.1f fps (%.1f vsyncs/s, full speed 50)", (double)(nsends - s0) / sec, n0 / sec); + char title[64]; + snprintf(title, sizeof title, "Project Solana - %.0f FPS", (double)(nsends - s0) / sec); + SetWindowTextA(win, title); + t0 = t; s0 = nsends; n0 = 0; + } + if (text[0]) { + SetBkMode(dc, OPAQUE); SetBkColor(dc, RGB(0, 0, 0)); SetTextColor(dc, RGB(255, 255, 0)); + TextOutA(dc, 4, 4, text, (int)strlen(text)); + } + } + ReleaseDC(win, dc); + } + return 1; +} + +/* a movie starts: forget key presses from before it (the Enter that chose it) */ +/* movies: a fresh press of Cross or Start on the controller skips (buttons already held when the movie starts do not) */ +static unsigned skip_held; +void plat_movie_started(void) { pump(); skip_held = plat_pad_buttons() & 0x4008; } +int plat_key_skip(void) +{ + pump(); + unsigned b = plat_pad_buttons() & 0x4008, fresh = b & ~skip_held; + skip_held = b; + return fresh != 0 || (getenv("UYA_SKIPMOVIES") && getenv("UYA_KEYS")); /* (testing: scripted runs skip movies) */ +} + +void plat_poll(void) +{ + if (!win) return; + pump(); + if (win_closed) { uya_log("window closed"); exit(0); } +} + +/* ---- sound: waveOut, one buffer per call ---- */ +static HWAVEOUT wo; +static WAVEHDR wh; +static int16_t *wbuf; + +void plat_audio_stop(void) +{ + if (!wo) return; + waveOutReset(wo); + waveOutUnprepareHeader(wo, &wh, sizeof wh); + waveOutClose(wo); + wo = 0; + free(wbuf); + wbuf = 0; +} + +void plat_audio_play(const int16_t *lr, size_t frames, int rate, const char *tag) +{ + WAVEFORMATEX fmt = {0}; + (void)tag; + plat_audio_stop(); + fmt.wFormatTag = WAVE_FORMAT_PCM; + fmt.nChannels = 2; + fmt.nSamplesPerSec = (DWORD)rate; + fmt.wBitsPerSample = 16; + fmt.nBlockAlign = 4; + fmt.nAvgBytesPerSec = (DWORD)rate * 4; + if (waveOutOpen(&wo, WAVE_MAPPER, &fmt, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR) { wo = 0; uya_log("sound: no output device"); return; } + wbuf = malloc(frames * 4); + if (!wbuf) { waveOutClose(wo); wo = 0; return; } + memcpy(wbuf, lr, frames * 4); + memset(&wh, 0, sizeof wh); + wh.lpData = (LPSTR)wbuf; + wh.dwBufferLength = (DWORD)(frames * 4); + waveOutPrepareHeader(wo, &wh, sizeof wh); + waveOutWrite(wo, &wh, sizeof wh); +} + +/* ---- sound effects stream: waveOut with a ring of buffers ---- */ +#define SBUFS 32 +#define SBUF_FRAMES 1024 +static HWAVEOUT so; +static WAVEHDR sh[SBUFS]; +static int16_t sbuf[SBUFS][SBUF_FRAMES * 2]; +static int snext, sopen_failed; + +static void stream_open(void) +{ + WAVEFORMATEX fmt = {0}; + fmt.wFormatTag = WAVE_FORMAT_PCM; + fmt.nChannels = 2; + fmt.nSamplesPerSec = 48000; + fmt.wBitsPerSample = 16; + fmt.nBlockAlign = 4; + fmt.nAvgBytesPerSec = 48000 * 4; + if (waveOutOpen(&so, WAVE_MAPPER, &fmt, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR) { so = 0; sopen_failed = 1; return; } + for (int i = 0; i < SBUFS; i++) { + memset(&sh[i], 0, sizeof sh[i]); + sh[i].lpData = (LPSTR)sbuf[i]; + sh[i].dwBufferLength = SBUF_FRAMES * 4; + waveOutPrepareHeader(so, &sh[i], sizeof sh[i]); + sh[i].dwFlags |= WHDR_DONE; + } +} + +int plat_stream_queued(void) +{ + int n = 0; + if (!so) return sopen_failed ? 1 << 20 : 0; + for (int i = 0; i < SBUFS; i++) if (!(sh[i].dwFlags & WHDR_DONE)) n += (int)(sh[i].dwBufferLength / 4); + return n; +} + +void plat_stream_write(const int16_t *lr, int frames) +{ + if (!so && !sopen_failed) stream_open(); + if (!so) return; + while (frames > 0) { + WAVEHDR *h = &sh[snext]; + if (!(h->dwFlags & WHDR_DONE)) return; /* ring full: drop */ + int n = frames > SBUF_FRAMES ? SBUF_FRAMES : frames; + memcpy(sbuf[snext], lr, (size_t)n * 4); + h->dwBufferLength = (DWORD)n * 4; + h->dwFlags &= ~WHDR_DONE; + waveOutWrite(so, h, sizeof *h); + snext = (snext + 1) % SBUFS; + lr += n * 2; + frames -= n; + } +} + +/* entry the launcher calls */ +__declspec(dllexport) int uya_main_win32(void) +{ + int argc = 0; + LPWSTR *wv = CommandLineToArgvW(GetCommandLineW(), &argc); + char **argv = calloc(argc + 1, sizeof *argv); + for (int i = 0; i < argc; i++) { + int n = WideCharToMultiByte(CP_UTF8, 0, wv[i], -1, 0, 0, 0, 0); + argv[i] = malloc(n); + WideCharToMultiByte(CP_UTF8, 0, wv[i], -1, argv[i], n, 0, 0); + } + return uya_main(argc, argv); +} +#endif diff --git a/games/rac1/host/src/rac1/boot.c b/games/rac1/host/src/rac1/boot.c new file mode 100644 index 0000000..5e897d7 --- /dev/null +++ b/games/rac1/host/src/rac1/boot.c @@ -0,0 +1,64 @@ +/* RAC1 start-up: what crt0 and main (src/core/boot.c, 0x0012DB18) do on the + * PS2, for the host. + * + * main runs the executable's game program (func_001E99D8: logos, intro movie, + * loading screen, sound bank, then the level load), then, forever, ParseBin + * (0x0012DA38, host/src/rac1/levels.c) puts the next level's program over the + * executable's main segment and returns its entry point, and main calls it. + * The level programs are compiled separately (src/overlays, docs/LEVELS.md). */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include + +void uya_gs_init(void); +extern void func_001E99D8(void); +void *func_0012DA38(void); + +void rac1_boot(void) +{ + uya_gs_init(); + uya_log("boot: GS ready"); +} + +/* Testing: UYA_LEVEL="N@V" asks for level N at vsync V, as the title menu's + * "go to level" (func_0022F4A0 in the executable) does, so a run can reach + * ParseBin without the menu (only while the executable's program runs). + * UYA_VSYNCS=N ends the run at vsync N (with the report of missing functions). */ +const void *rac1_current_level(void); +void rac1_test_hooks(void) +{ + static int done; + static long stop = -2; + if (stop == -2) { const char *s = getenv("UYA_VSYNCS"); stop = s ? atol(s) : -1; } + if (stop > 0 && uya_frame_count() >= (uint32_t)stop) { uya_log("UYA_VSYNCS=%ld reached", stop); exit(0); } + const char *e = getenv("UYA_LEVEL"); + if (!e || done || rac1_current_level()) return; + int lvl = atoi(e); + const char *at = strchr(e, '@'); + if (uya_frame_count() < (at ? (uint32_t)atoi(at + 1) : 0u)) return; + done = 1; + uya_log("UYA_LEVEL: asking for level %d at vsync %u", lvl, uya_frame_count()); + ((void (*)(int))(uintptr_t)0x0022F4A0)(lvl); +} + +void rac1_run(void) +{ + { /* The EE's FPU and the VUs round toward zero (truncate), not to nearest. The game + * depends on it: level 06 counts a timer down by 0.1 while it is > 0, and only a + * truncating subtraction ends at or below zero on the step the game expects (to + * nearest leaves a tiny positive rest, one step more makes a moby's scale negative, + * its bounding cells cross, the grid frees cells twice and a pointer runs off RAM). + * MXCSR RC = 11 on this thread (the game's); the GS threads exist already and keep + * round to nearest. Denormals are left alone (VU MAX/MINI compare integer patterns). */ + unsigned m = __builtin_ia32_stmxcsr(); + __builtin_ia32_ldmxcsr((m & ~0x6000u) | 0x6000u); + } + uya_log("calling the executable's game program (func_001E99D8)"); + func_001E99D8(); + for (;;) { + void (*entry)(void) = (void (*)(void))func_0012DA38(); + uya_log("main: calling the program's entry point 0x%08X", (u32)(uintptr_t)entry); + entry(); + } +} diff --git a/games/rac1/host/src/rac1/core_map.c b/games/rac1/host/src/rac1/core_map.c new file mode 100644 index 0000000..d6a7c4a --- /dev/null +++ b/games/rac1/host/src/rac1/core_map.c @@ -0,0 +1,359 @@ +/* RAC1 core: the Sony libraries, newlib and kernel calls the game makes, by + * their PAL (SCES_509.16) addresses. + * + * The UYA Wrapper's implementations are kept under host/src/sdk with the + * prefix uyafn_ (their UYA addresses). Where RAC1's library code is the same + * function (libgraph, libdma: matched instruction for instruction, see + * docs/RAC1_NOTES.md) the RAC1 address forwards to it. The rest is written + * here from OpenRAC's own C for these functions (src/core). */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include +#include +#include +#include + +/* ---- newlib ------------------------------------------------------------- */ +void *func_00115248(void *d, const void *s, u32 n) { return memmove(d, s, n); } /* memcpy */ +void *func_001153FC(void *d, s32 c, u32 n) { return memset(d, c, n); } /* memset */ +s32 func_001151B4(const void *a, const void *b, u32 n) { return memcmp(a, b, n); } /* memcmp */ +char *func_00116428(const char *s, s32 c) { return strchr(s, c); } /* strchr */ +char *func_001166FC(char *d, const char *s) { return strcpy(d, s); } /* strcpy */ +u32 func_00116810(const char *s) { return (u32)strlen(s); } /* strlen */ +s32 func_001165B8(const char *a, const char *b) { return strcmp(a, b); } /* strcmp */ +s32 func_00116948(const char *a, const char *b, u32 n) { return strncmp(a, b, n); } /* strncmp */ +char *func_00116B00(char *d, const char *s, u32 n) { return strncpy(d, s, n); } /* strncpy */ +char *func_00116CC0(const char *a, const char *b) { return strstr(a, b); } /* strstr */ + +/* sprintf (0x00116248) */ +int func_00116248(char *buf, const char *fmt, ...) +{ + va_list ap; va_start(ap, fmt); + int n = vsprintf(buf, fmt, ap); + va_end(ap); + return n; +} + +/* srand / rand: newlib's LCG on the reent structure's seed, *(D_0012F86C) + 0x58 */ +#define RAND_SEED ((u32 *)(uintptr_t)(*(u32 *)0x0012F86C + 0x58)) +void func_001160C8(s32 seed) { *RAND_SEED = (u32)seed; } +s32 func_001160D8(void) { return (s32)((*RAND_SEED = *RAND_SEED * 1103515245u + 12345u) & 0x7FFFFFFF); } + +/* scePrintf (libkernl, 0x0011A6C8): to the log */ +int func_0011A6C8(const char *fmt, ...) +{ + char b[1024]; + va_list ap; va_start(ap, fmt); + int n = vsnprintf(b, sizeof b, fmt, ap); + va_end(ap); + while (n > 0 && n < (int)sizeof b && (b[n - 1] == '\n' || b[n - 1] == '\r')) b[--n] = 0; + uya_log("game: %s", b); + return n; +} +void func_00124B60(void *fmt, ...) { (void)fmt; } /* libpad2's debug print, compiled out */ + +/* ---- kernel syscalls (0x00118A40-0x00118EB0, one 16-byte stub each) ---- */ +s32 func_00118D80(s32 mode) { (void)mode; return 0; } /* FlushCache */ +s32 func_00118EA0(void) { return 0x02000000; } /* GetMemorySize */ +/* DMAC interrupt handlers: host/src/sdk/dma.c calls them as the chains run + * (RAC1's VU1 chain handler, func_00235118, clears the chain-stage bits the + * draw code waits on and restarts the DMA). */ +s32 uyafn_11EB30(s32 ch, s32 (*h)(s32), s32 next); +s32 uyafn_11F9A8(s32 ch); +s32 uya_dmac_remove(s32 ch); +s32 uya_dmac_disable(s32 ch); +s32 func_00118AB0(s32 ch, s32 (*h)(s32), s32 next) { return uyafn_11EB30(ch, h, next); } /* AddDmacHandler (0x12) */ +s32 func_00118AD0(s32 ch, s32 id) { (void)id; return uya_dmac_remove(ch); } /* RemoveDmacHandler (0x13) */ +s32 func_00118B00(s32 ch) { return uyafn_11F9A8(ch); } /* _EnableDmac (0x16) */ +s32 func_00118B10(s32 ch) { return uya_dmac_disable(ch); } /* _DisableDmac (0x17) */ +s32 func_00119460(s32 ch) { return uyafn_11F9A8(ch); } /* EnableDmac (libkernl, DI/EI around 0x16) */ +s32 func_001193F8(s32 ch) { return uya_dmac_disable(ch); } /* DisableDmac */ +static s32 next_sema = 1; +s32 func_00118C70(void *param) { (void)param; return next_sema++; } /* CreateSema (0x40) */ +s32 func_00118C80(s32 id) { (void)id; return 0; } /* DeleteSema */ +s32 func_00118C90(s32 id) { (void)id; return 0; } /* SignalSema */ +s32 func_00118CB0(s32 id) { return id; } /* WaitSema */ +s32 func_00118CC0(s32 id) { return id; } /* PollSema */ +s32 func_00118BE0(void) { return 1; } /* GetThreadId (0x2F) */ +s32 func_00118E10(s32 id) { (void)id; return -1; } /* sceSifDmaStat (0x76): done */ +/* IOP memory, as far as the game's own data goes there: sceSifSetDma's EE -> + * IOP transfers land here, and the stash server (IOPSTASH.IRX, below) hands + * it back */ +#define IOP_RAM_SIZE (2u << 20) +static u8 iop_ram[IOP_RAM_SIZE] __attribute__((aligned(16))); +s32 func_00118E20(void *dma, s32 n) /* sceSifSetDma (0x77): {src, dest, size, attr} x n */ +{ + const u32 *d = (const u32 *)dma; + for (s32 i = 0; d && i < n; i++, d += 4) { + u32 src = d[0], dst = d[1] & 0x1FFFFFu, size = d[2]; + if ((src >> 28) == 2 || (src >> 28) == 3) src &= 0x0FFFFFFFu; /* uncached views */ + if (dst < IOP_RAM_SIZE && size <= IOP_RAM_SIZE - dst && src >= PS2_RAM_LO && src + size <= PS2_RAM_HI) + memcpy(iop_ram + dst, (const void *)(uintptr_t)src, size); + } + return 1; +} +s32 func_00118E70(s32 reg) { (void)reg; return 0x00070000; } /* sceSifGetReg (0x7A): IOP booted, SIF ready */ + +/* ---- SIF RPC (libkernl): no IOP. A call completes at once with a zeroed + * reply, which the game's wrappers read as "nothing to report", except for + * the servers the host provides: + * - 0x11, IOPSTASH.IRX (the game's "stash", src/game/stash.c): a buffer of + * 0x94000 bytes in IOP memory (the module's stash_ram) where the game keeps + * data it fetches back later (the pause menu's weapon models and + * animations). RPC 2 answers {base, size}; RPC 1 gets {IOP address} and + * sends back the receive size's worth of IOP memory from there (the + * module's rpc_stash_mgr returns that address as its reply buffer). The + * game sends the data with sceSifSetDma (above). Without it every stash + * slot was refused (size 0) and the pause menu unpacked the weapon data + * from a scrubbed buffer (a crash reading 0xDEE6BEEF). ---------- */ +#define STASH_BASE 0x00160000u +#define STASH_SIZE 0x00094000u +void func_0011AE20(s32 mode) { (void)mode; } /* sceSifInitRpc */ +s32 func_0011B2F8(void *cd, u32 server, s32 mode) /* sceSifBindRpc */ +{ + (void)mode; + if (cd) { ((u32 *)cd)[9] = 0x00F00000 | (server & 0xFFFF); } /* a nonzero server pointer */ + return 0; +} +s32 func_0011B4C8(void *cd, s32 fno, s32 mode, void *send, s32 ssize, void *recv, s32 rsize, + void (*end)(void *), void *endparam) /* sceSifCallRpc */ +{ + (void)mode; (void)ssize; + u32 server = cd ? ((u32 *)cd)[9] & 0xFFFF : 0; + if (recv && rsize > 0) memset(recv, 0, (size_t)rsize); + if (server == 0x11 && recv) { /* IOPSTASH */ + if (fno == 2 && rsize >= 8) { ((u32 *)recv)[0] = STASH_BASE; ((u32 *)recv)[1] = STASH_SIZE; } + else if (fno == 1 && send) { + u32 src = ((const u32 *)send)[0] & 0x1FFFFFu; + if (rsize > 0 && src < IOP_RAM_SIZE && (u32)rsize <= IOP_RAM_SIZE - src) memcpy(recv, iop_ram + src, (size_t)rsize); + } + } + if (end) end(endparam); + return 0; +} +s32 func_0011B6B8(void *cd) { (void)cd; return 0; } /* sceSifCheckStatRpc: idle */ + +/* ---- libgraph and libdma: the UYA implementations ------------------------ */ +void uyafn_12BEE8(s32 mode, s32 inter, s32 omode, s32 ffmode); +void uyafn_12C138(u64 *disp, s32 psm, s32 w, s32 h, s32 dx, s32 dy); +void uyafn_12C4B0(u64 *disp); +s16 uyafn_12C570(s16 psm, s16 w, s16 h); +s32 uyafn_12C638(u64 *d, s16 psm, s16 w, s16 h, s16 ztest, s16 zpsm); +s32 uyafn_12C908(s32 mode); +s32 uyafn_12C9A0(u64 *lp, s32 dbp, s32 dbw, s32 dpsm, s32 x, s32 y, s32 w, s32 h); +s32 uyafn_12CCC8(u64 *lp, void *src); +s32 uyafn_12D4D8(s32 fn); +void *uyafn_12D650(s32 id); +s32 uyafn_12D938(void *chan, void *tag); +void uyafn_12A950(void); +void *uyafn_12C078(void); +void uya_gs_gif(const u32 *d, u32 qwc); + +void func_00121B78(s16 mode, s16 inter, s16 out, s16 ff) { uyafn_12BEE8(mode, inter, out, ff); } /* sceGsResetGraph */ +void *func_00121D08(void) { return uyafn_12C078(); } /* sceGsGetGParam */ +void func_00121DC8(u64 *d, s32 psm, s32 w, s32 h, s32 dx, s32 dy) { uyafn_12C138(d, psm, w, h, dx, dy); } /* sceGsSetDefDispEnv */ +void func_00122140(u64 *d) { uyafn_12C4B0(d); } /* sceGsPutDispEnv */ +s16 func_00122200(s16 psm, s16 w, s16 h) { return uyafn_12C570(psm, w, h); } +s32 func_001222C8(u64 *d, s16 psm, s16 w, s16 h, s16 zt, s16 zpsm) { return uyafn_12C638(d, psm, w, h, zt, zpsm); } /* sceGsSetDefDrawEnv */ +s32 func_00122598(s32 mode) { return uyafn_12C908(mode); } /* sceGsSyncV */ +/* testing: UYA_SICHECK=v also logs sceGsSetDefLoadImage calls from vsync v */ +s32 func_00122630(u64 *lp, s32 dbp, s32 dbw, s32 dpsm, s32 x, s32 y, s32 w, s32 h) /* sceGsSetDefLoadImage */ +{ + static int n; static long v0 = -2; + if (v0 == -2) { const char *e = getenv("UYA_SICHECK"); v0 = e ? atol(e) : -1; } + if (v0 >= 0 && (long)uya_frame_count() >= v0 && n++ < 400) + uya_log("SetDefLoadImage v%u: lp %08X bp %X bw %d psm %X (%d,%d) %dx%d", uya_frame_count(), (u32)(uintptr_t)lp, dbp, dbw, dpsm, x, y, w, h); + return uyafn_12C9A0(lp, dbp, dbw, dpsm, x, y, w, h); +} +s32 func_00122958(u64 *lp, void *src) { return uyafn_12CCC8(lp, src); } /* sceGsExecLoadImage */ +/* sceGsSetDefStoreImage (0x00122818) / sceGsExecStoreImage (0x00122AD8): GS -> memory. + * The pause menu saves VRAM strips with them before it draws over that memory. */ +void uya_gs_store_image(u32 bp, u32 bw, u32 psm, u32 x, u32 y, u32 w, u32 h, void *dst); +s32 func_00122818(s32 *sp, s32 sbp, s32 sbw, s32 spsm, s32 x, s32 y, s32 w, s32 h) +{ + sp[0] = 0x53544F52; sp[1] = sbp; sp[2] = sbw; sp[3] = spsm; sp[4] = x; sp[5] = y; sp[6] = w; sp[7] = h; + return 0; +} +s32 func_00122AD8(s32 *sp, void *dst) +{ + if (sp[0] != 0x53544F52) return -1; + { static int n; if (getenv("UYA_SICHECK") && n++ < 40) uya_log("StoreImage: bp %X bw %d psm %X (%d,%d) %dx%d to %08X", sp[1], sp[2], sp[3], sp[4], sp[5], sp[6], sp[7], (u32)(uintptr_t)dst); } + { /* PS2 uncached / accelerated mirrors of main RAM */ + u32 a = (u32)(uintptr_t)dst; + if (a >= 0x20000000 && a < 0x40000000) dst = (void *)(uintptr_t)(a & 0x0FFFFFFF); + } + uya_gs_store_image((u32)sp[1], (u32)sp[2], (u32)sp[3], (u32)sp[4], (u32)sp[5], (u32)sp[6], (u32)sp[7], dst); + return 0; +} +s32 func_00123168(s32 fn) { return uyafn_12D4D8(fn); } /* sceGsSyncVCallback */ +void *func_001232E0(s32 id) { return uyafn_12D650(id); } /* sceDmaGetChan */ +s32 func_001235C8(void *chan, void *tag) { return uyafn_12D938(chan, tag); } /* sceDmaSend */ +void func_001207B8(void) { uyafn_12A950(); } /* sceGsResetPath */ +s32 func_00123308(s32 mode) { (void)mode; return 0; } /* sceDmaReset */ + +/* sceGsPutDrawEnv (0x001224B0): a GIF packet (GIFtag + A+D pairs) over channel 2 */ +s32 func_001224B0(void *pkt) +{ + u32 *q = (u32 *)pkt; + u32 nloop = q[0] & 0x7FFF; + { extern const char *uya_gs_src; uya_gs_src = "PATH3 (GS library)"; } + uya_gs_gif(q, nloop + 1); + return 0; +} + +/* ---- libcdvd ------------------------------------------------------------ */ +/* sceCdCallback (0x00120978): the handler runs when a read completes. Reads + * finish at once here, so it is owed and delivered at the next sceCdSync or + * vsync, after the caller has recorded the request (RAC1's stream code sets + * its "busy" flag right after starting the read; the callback clears it). */ +static void (*cd_cb)(s32); +static int cd_cb_owed; +void *func_00120978(void (*cb)(s32)) { void *old = (void *)cd_cb; cd_cb = cb; return old; } +void rac1_cd_service(void) +{ + if (cd_cb_owed && cd_cb) { cd_cb_owed = 0; cd_cb(1); } +} +s32 func_00121750(s32 lsn, s32 n, void *buf, void *mode) /* sceCdRead: done at once */ +{ + cd_cb_owed = 1; + (void)mode; + static int log = -1; + if (log < 0) log = getenv("UYA_DISCLOG") != 0; + if (log) uya_log("disc read LSN %d x%d -> 0x%08X", lsn, n, (u32)(uintptr_t)buf); + if (uya_disc_read((u32)lsn, (u32)n, buf)) uya_log("disc read failed at LSN %d", lsn); + return 1; +} +s32 func_00120F30(s32 mode) { (void)mode; rac1_cd_service(); return 0; } /* sceCdSync: idle */ +s32 func_00121930(void) { return 0; } /* sceCdGetError: none */ +s32 func_001219C8(void) { return 1; } /* sceCdBreak */ +s32 func_00120D28(s32 x) { (void)x; return 1; } /* cd_check_ncmd: ready */ +s32 func_00121040(s32 x) { (void)x; return 1; } /* cd_check_scmd: ready */ +s32 func_00121A80(u8 *clk) /* sceCdReadClock: BCD */ +{ + time_t t = time(0); struct tm *m = localtime(&t); +#define BCD(v) (u8)((((v) / 10) << 4) | ((v) % 10)) + if (clk) { clk[0] = 0; clk[1] = BCD(m->tm_sec); clk[2] = BCD(m->tm_min); clk[3] = BCD(m->tm_hour); + clk[4] = 0; clk[5] = BCD(m->tm_mday); clk[6] = BCD(m->tm_mon + 1); clk[7] = BCD(m->tm_year % 100); } +#undef BCD + return 1; +} + +/* ---- IOP start-up (InitOnce, func_00201E88): there is no IOP; every step succeeds ---- */ +s32 func_0011D248(char *img) { uya_log("IOP reboot with %s (no IOP on the host)", img); return 1; } /* sceSifRebootIop */ +s32 func_0011D210(void) { return 1; } /* sceSifSyncIop */ +s32 func_0011D960(void) { return 0; } /* sceSifInitIopHeap */ +void func_0011D9A8(void) {} +s32 func_0011CB40(void) { return 0; } +s32 func_0011BF48(void) { return 0; } /* sceFsReset */ +s32 func_001211B0(s32 mode) { (void)mode; return 1; } /* sceCdInit */ +s32 func_00121490(s32 mode) { (void)mode; return 1; } /* sceCdMmode / sync */ +s32 func_00121688(s32 mode) { (void)mode; return 2; } /* sceCdDiskReady: ready */ +s32 func_00118D60(s32 mode) { (void)mode; return 0; } /* syscall 0x61 */ + +/* ---- game helpers in hand-written assembly ---- */ +void uya_dma_poll(void); +/* 0x001F9988: a short busy wait (the game's DMA wait loops call it while a + * channel's STR bit is set); here it runs the pending transfers. */ +void func_001F9988(s32 n) { (void)n; uya_dma_poll(); } + +/* printf (newlib, 0x00116078): to the log */ +int func_00116078(const char *fmt, ...) +{ + char b[1024]; + va_list ap; va_start(ap, fmt); + int n = vsnprintf(b, sizeof b, fmt, ap); + va_end(ap); + while (n > 0 && n < (int)sizeof b && (b[n - 1] == '\n' || b[n - 1] == '\r')) b[--n] = 0; + uya_log("game: %s", b); + return n; +} + +/* ---- libmc: the UYA Wrapper's folder-backed card (host/src/sdk/memcard.c) ---- */ +s32 uyafn_12E2E0(s32 mode, s32 *cmd, s32 *result); +s32 uyafn_12E400(s32 port, s32 slot, s32 *type, s32 *free_kb, s32 *format); +s32 uyafn_12E580(s32 port, s32 slot, char *name, s32 mode, s32 maxent, void *table); +s32 uyafn_12DCA0(s32 port, s32 slot, char *name, s32 mode); +s32 uyafn_12DDC8(s32 port, s32 slot, char *name); +s32 uyafn_12DE00(s32 fd); +s32 uyafn_12DEC0(s32 fd, s32 off, s32 whence); +s32 uyafn_12E050(s32 fd, void *buf, s32 size); +s32 uyafn_12E168(s32 fd, void *buf, s32 size); +s32 uyafn_12E7A8(s32 port, s32 slot, char *name); +s32 uyafn_12E6D8(s32 port, s32 slot); +s32 uyafn_12E8D8(s32 port, s32 slot); +s32 func_001236F0(void) { return 0; } /* sceMcInit */ +s32 func_001238B0(s32 port, s32 slot, char *name, s32 mode) { return uyafn_12DCA0(port, slot, name, mode); } /* sceMcOpen */ +s32 func_001239D8(s32 port, s32 slot, char *name) { return uyafn_12DDC8(port, slot, name); } /* sceMcMkdir */ +s32 func_00123A10(s32 fd) { return uyafn_12DE00(fd); } /* sceMcClose */ +s32 func_00123AC8(s32 fd, s32 off, s32 whence) { return uyafn_12DEC0(fd, off, whence); } /* sceMcSeek */ +s32 func_00123C30(s32 fd, void *buf, s32 n) { return uyafn_12E050(fd, buf, n); } /* sceMcRead */ +s32 func_00123D48(s32 fd, void *buf, s32 n) { return uyafn_12E168(fd, buf, n); } /* sceMcWrite */ +s32 func_00123F30(s32 mode, s32 *cmd, s32 *result) { return uyafn_12E2E0(mode, cmd, result); } /* sceMcSync */ +s32 func_00124068(s32 port, s32 slot, s32 *type, s32 *free_kb, s32 *format) { return uyafn_12E400(port, slot, type, free_kb, format); } /* sceMcGetInfo */ +s32 func_001241F0(s32 port, s32 slot, char *name, s32 mode, s32 n, void *table) { return uyafn_12E580(port, slot, name, mode, n, table); } /* sceMcGetDir */ +s32 func_00124340(s32 port, s32 slot) { return uyafn_12E6D8(port, slot); } /* sceMcFormat (RPC 0x10) */ +s32 func_00124410(s32 port, s32 slot, char *name) { return uyafn_12E7A8(port, slot, name); } /* sceMcDelete */ +s32 func_00124528(s32 port, s32 slot) { return uyafn_12E8D8(port, slot); } /* sceMcUnformat (RPC 0x11) */ +s32 func_00124650(void) { return 0; } /* bind the mc/pad IOP servers */ + +/* ---- libscf ---- */ +s32 func_0012D380(void) { return 1; } /* system language: 1 = English (InitOnce maps it to the game's language 0) */ +void func_0012D818(u8 *clk) { (void)clk; } /* sceScfGetLocalTimefromRTC: sceCdReadClock (func_00121A80) already gives local time */ + +/* ---- libpad2 ---- */ +unsigned plat_pad_buttons(void); +int plat_pad_sticks(u8 st[4]); /* left X, left Y, right X, right Y (0x7F centre); 0: no gamepad */ +s32 func_00124B88(void) { return 1; } /* scePad2Init */ +s32 func_00124BC8(void *param, char *buf) { (void)param; (void)buf; return 0; } /* scePad2CreateSocket: socket 0 */ +s32 func_00124EE0(s32 port) { (void)port; return 1; } /* scePad2GetState: 1 = ready (UpdatePad, pad.c) */ +s32 func_00124DF0(s32 port, u8 *buf) { (void)port; if (buf) memset(buf, 0, 4); return 0; } /* button profile: none */ +s32 func_00125218(s32 port, u8 *buf) { (void)port; if (buf) memset(buf, 0, 4); return 0; } +/* scePad2Read(socket, buf): buf[0] = Select..Left, buf[1] = L2..Square (both inverted), then the + * four sticks (0x7F = centre) and twelve pressures, as UpdatePad's report parser (func_002181F0) reads them. */ +s32 func_00124D18(s32 id, u8 *buf) +{ + (void)id; + unsigned b = id == 0 ? plat_pad_buttons() : 0; + buf[0] = (u8)~(b & 0xFF); + buf[1] = (u8)~(b >> 8); + for (int i = 2; i < 6; i++) buf[i] = 0x7F; + /* bits 16..19 (UYA_KEYS lup/ldown/lleft/lright): the left stick, buf[4] X and buf[5] Y */ + if (b & 0x40000) buf[4] = 0x00; else if (b & 0x80000) buf[4] = 0xFF; + if (b & 0x10000) buf[5] = 0x00; else if (b & 0x20000) buf[5] = 0xFF; + /* bits 20..23: the right stick, buf[2] X and buf[3] Y */ + if (b & 0x400000) buf[2] = 0x00; else if (b & 0x800000) buf[2] = 0xFF; + if (b & 0x100000) buf[3] = 0x00; else if (b & 0x200000) buf[3] = 0xFF; + { /* a gamepad's sticks (Windows: XInput), where it has one and they are off centre */ + u8 st[4]; + if (id == 0 && plat_pad_sticks(st)) { + if (st[0] != 0x7F) buf[4] = st[0]; + if (st[1] != 0x7F) buf[5] = st[1]; + if (st[2] != 0x7F) buf[2] = st[2]; + if (st[3] != 0x7F) buf[3] = st[3]; + } + } + /* pressures: right, left, up, down, triangle, circle, cross, square, L1, R1, L2, R2 */ + static const int bit[12] = { 5, 7, 4, 6, 12, 13, 14, 15, 10, 11, 8, 9 }; + for (int i = 0; i < 12; i++) buf[6 + i] = (b >> bit[i]) & 1 ? 0xFF : 0; + { /* testing: UYA_PADLOG=1 logs the report whenever it changes */ + static int on = -1; static u8 last[6]; + if (on < 0) on = getenv("UYA_PADLOG") != 0; + static int hero = -1; + if (hero < 0) hero = getenv("UYA_HEROLOG") != 0; + if (hero && id == 0) { /* testing: the hero's position, yaw, velocity and state (D_0013F450) */ + const float *m = (const float *)(uintptr_t)0x0013F4D0; + const u8 *pd = (const u8 *)(uintptr_t)0x0013CA40; + int ix = (*(const s16 *)(pd + 0x18E) + 29) % 30; + uya_log("v%u hero: pos %.2f %.2f %.2f yaw %.3f vel %.3f %.3f %.3f state %d | pad axes %.2f %.2f %.2f %.2f held %X ang %.3f mag %.3f", uya_frame_count(), m[0], m[1], m[2], m[6], m[24], m[25], m[26], *(const s32 *)(uintptr_t)(0x0013F450 + 0x198), + ((const float *)(pd + 0x100))[0], ((const float *)(pd + 0x100))[1], ((const float *)(pd + 0x100))[2], ((const float *)(pd + 0x100))[3], *(const s32 *)(pd + 0x1A0), + ((const float *)(pd + 0x258))[ix], ((const float *)(pd + 0x2D0))[ix]); + const float *g = (const float *)(uintptr_t)0x0015EE60; + const u8 *h = (const u8 *)(uintptr_t)0x0013F450; + uya_log(" scales %g %g %g %g %g | hstate %d f180 %.3f f190 %.4f f194 %.4f f414 %.3f f45C %.3f i208C %d", g[0], g[1], g[2], g[3], g[4], + *(const s32 *)(h + 0x2084), *(const float *)(h + 0x180), *(const float *)(h + 0x190), *(const float *)(h + 0x194), *(const float *)(h + 0x414), *(const float *)(h + 0x45C), *(const s32 *)(h + 0x208C)); + } + if (on && id == 0 && memcmp(last, buf, 6)) { memcpy(last, buf, 6); uya_log("pad: buttons %02X%02X RX %02X RY %02X LX %02X LY %02X", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]); } + } + return 18; +} diff --git a/games/rac1/host/src/rac1/levels.c b/games/rac1/host/src/rac1/levels.c new file mode 100644 index 0000000..9387861 --- /dev/null +++ b/games/rac1/host/src/rac1/levels.c @@ -0,0 +1,86 @@ +/* Level programs (docs/LEVELS.md). + * + * ParseBin (0x0012DA38, src/core/boot.c) is the PS2's loader for the next + * program: by the time main calls it, the game has read the level's code + * overlay from the disc into the buffer D_0015EF4C points at (its first word + * is the overlay's offset). ParseBin copies the overlay's records (lit, bss, + * data, the three vtables, text: (address, size, type, entry) and the bytes) + * over the executable's main segment while their entry point agrees, and + * returns it. The host does the same copy, so every data record lands where + * the PS2 puts it, and then switches the code: the level's text becomes int3 + * (like all PS2 code space), the level's trampolines go in at its function + * addresses (its C from src/overlays, its translated functions), and the + * recompiled code's dispatch table becomes the level's. The entry point it + * returns is then a trampoline into the level's own code. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include "rac1_levels.h" +#include +#include + +#define D_0015EF4C (*(u32 *)0x0015EF4C) /* core_lit: the level buffer */ +#define EXE_TEXT_LO 0x001E9080u /* the executable's game program */ +#define EXE_TEXT_HI 0x0023E740u + +struct chunk { u32 dst, size, pad8, id; }; + +static const rac1_level_t *current; +const rac1_level_t *rac1_current_level(void) { return current; } + +/* int3 over [lo, hi) except where a record of the program just copied lies */ +static void trap_fill_outside(u32 lo, u32 hi, const struct chunk *const *recs, int n) +{ + while (lo < hi) { + u32 end = hi; + int inside = 0; + for (int i = 0; i < n; i++) { + u32 a = recs[i]->dst, b = recs[i]->dst + recs[i]->size; + if (a <= lo && lo < b) { lo = b; inside = 1; break; } + if (lo < a && a < end) end = a; + } + if (inside) continue; + uya_fill_code_traps(lo, end); + lo = end; + } +} + +/* ParseBin (0x0012DA38) */ +void *func_0012DA38(void) +{ + u8 *p = (u8 *)(uintptr_t)(D_0015EF4C + *(u32 *)(uintptr_t)D_0015EF4C); + const struct chunk *recs[16]; + int n = 0; + u32 id = 0, text_addr = 0, text_bytes = 0; + for (;;) { + const struct chunk *c = (const struct chunk *)p; + if (id == 0) id = c->id; + else if (c->id != id) break; + if (c->dst < PS2_RAM_LO || c->dst + c->size > PS2_RAM_HI || n == 16) { + uya_log("ParseBin: record %d at 0x%08X (+0x%X) is outside RAM; stopping", n, c->dst, c->size); + exit(1); + } + memcpy((void *)(uintptr_t)c->dst, c + 1, c->size); + uya_log("ParseBin: record %d: 0x%08X-0x%08X (type %u)", n, c->dst, c->dst + c->size, c->pad8); + if (c->dst <= id && id < c->dst + c->size) { text_addr = c->dst; text_bytes = c->size; } + recs[n++] = c; + p += sizeof *c + c->size; + } + const rac1_level_t *lv = 0; + for (int i = 0; i < rac1_level_count; i++) + if (rac1_levels[i].entry == id && rac1_levels[i].text_addr == text_addr && rac1_levels[i].text_bytes == text_bytes) + lv = &rac1_levels[i]; + if (!lv) { + uya_log("ParseBin: a program with entry 0x%08X and text 0x%08X (+0x%X) was loaded; no level program built into the " + "wrapper matches it (build.py --levels)", id, text_addr, text_bytes); + exit(0); + } + /* code: the executable's game program and the level's text are PS2 code + * space no more and not yet (int3), then the level's trampolines */ + trap_fill_outside(EXE_TEXT_LO, EXE_TEXT_HI, recs, n); + uya_fill_code_traps(text_addr, text_addr + text_bytes); + uya_install_tramp_table(lv->tramps, *lv->tramp_count); + rc_set_table(lv->rc_table, *lv->rc_count); + current = lv; + uya_log("ParseBin: level %02d's program is in (%d records), entry 0x%08X (vsync %u)", lv->id, n, id, uya_frame_count()); + return (void *)(uintptr_t)id; +} diff --git a/games/rac1/host/src/rac1/rc_runtime.c b/games/rac1/host/src/rac1/rc_runtime.c new file mode 100644 index 0000000..d11ead0 --- /dev/null +++ b/games/rac1/host/src/rac1/rc_runtime.c @@ -0,0 +1,152 @@ +/* Runtime for the recompiled assembly-only functions (tools/recomp.py, + * host/include/rc.h): VU0 state, the stack the translated code runs on, + * calls by address and the few instructions that reach the host. */ +#include "rc.h" +#include +#include +void uya_log(const char *fmt, ...); +void uya_unimpl(const char *name); +uint32_t uya_frame_count(void); + +RcVu0 rc_vu0 = { .vf = { [0] = {0, 0, 0, 1} } }; + +/* the translated code's stack: 1 MB of host memory (DMA from it works, see sdk/dma.c) */ +static u8 rc_stack[1 << 20] __attribute__((aligned(64))); +u32 rc_sp = 0; + +/* the x86 stack (see RC_QA in rc.h): from a little above the first call into + * translated code down 8 MB, never overlapping PS2 memory (if the stack lies + * below 32 MB, as it can on Windows, the exception is off) */ +u32 rc_cstack_lo, rc_cstack_hi; + +#include +/* testing: UYA_NANWATCH=addr (hex) stops the run (SIGILL, for a debugger) at the + * first entry into or dispatch from translated code that finds the float at addr + * NaN or infinite, and logs where the check before and this one were made */ +static void nan_check(const char *where, u32 a) +{ + static u32 w = 1; static void *prev; static u32 preva; + if (w == 1) { const char *e = getenv("UYA_NANWATCH"); w = e ? (u32)strtoul(e, 0, 16) : 0; } + if (!w) return; + u32 v = *(u32 *)(uintptr_t)w; + if ((v & 0x7F800000u) == 0x7F800000u) { uya_log("NANWATCH: %08X became %08X; previous check %p/%08X, now %s %p/%08X", w, v, prev, preva, where, __builtin_return_address(1), a); __builtin_trap(); } + prev = __builtin_return_address(1); preva = a; +} +void rc_enter(RcCtx *c) +{ + nan_check("enter", 0); + if (!rc_cstack_hi) { + u32 here = (u32)(uintptr_t)__builtin_frame_address(0); + /* the x86 stack: above PS2 RAM (0-0x01FFFFFF), which Windows' main + * thread stack often is just above (0x03xxxxxx), Linux's far above */ + if (here > 0x02100000u) { + rc_cstack_hi = here < 0xFFF00000u ? here + (1u << 20) : 0xFFFFFFFFu; + rc_cstack_lo = here - (8u << 20); + if (rc_cstack_lo < 0x02000000u) rc_cstack_lo = 0x02000000u; + } else rc_cstack_hi = 1; /* checked: empty range */ + uya_log("x86 stack for translated code: %08X-%08X", rc_cstack_lo, rc_cstack_hi); + } + memset(c, 0, sizeof *c); + if (!rc_sp) rc_sp = (u32)(uintptr_t)(rc_stack + sizeof rc_stack - 64); + c->r[29].d[0] = (rc_sp - 64) & ~15u; + c->r[28].d[0] = RC_GP; +} +void rc_leave(RcCtx *c) { (void)c; } + +typedef void (*rc_fn)(RcCtx *); +typedef struct { u32 addr; rc_fn fn; } rc_row; +extern const rc_row rc_table[]; +extern const int rc_table_count; +void uya_trap_call(uint32_t ps2_addr, uint32_t ret_addr, const uint32_t *args); + +/* The table in use: the executable's (rc_table) until ParseBin loads a level + * (rcNN_table, host/src/rac1/levels.c). A level's table has its own + * functions and thunks for the core functions it calls. */ +static const rc_row *cur_table = rc_table; +static int cur_count = -1; +void rc_set_table(const rc_row *table, int count) { cur_table = table; cur_count = count; } + +void rc_dispatch(RcCtx *c, u32 addr) +{ + nan_check("dispatch", addr); + if (cur_count < 0) cur_count = rc_table_count; + int lo = 0, hi = cur_count - 1; + while (lo <= hi) { + int m = (lo + hi) / 2; + if (cur_table[m].addr == addr) { cur_table[m].fn(c); return; } + if (cur_table[m].addr < addr) lo = m + 1; else hi = m - 1; + } + u32 args[4] = { (u32)c->r[4].d[0], (u32)c->r[5].d[0], (u32)c->r[6].d[0], (u32)c->r[7].d[0] }; + uya_trap_call(addr, (u32)c->r[31].d[0], args); + c->r[2].d[0] = 0; +} + +/* rc_vcallms: host/src/gfx/vu.c (translated VU0 microprograms) */ + +void rc_syscall(RcCtx *c) { uya_log("recompiled code: syscall %d", (int)(s32)c->r[3].d[0]); } +void rc_break(u32 pc) { uya_log("recompiled code: break/trap at 0x%08X", pc); } +void rc_bad(u32 pc, const char *what) +{ + static int n; + if (n++ < 64) uya_log("recompiled code: instruction %s at 0x%08X is not supported", what, pc); +} +u32 rc_cop0(int r) +{ + uint32_t uya_frame_us(void); + if (r == 9) return (u32)((unsigned long long)uya_frame_count() * uya_frame_us() * 294912u / 1000u); /* Count: 294.912 MHz, one field per vsync (20 ms PAL) */ + return 0; +} + +/* testing: UYA_QUADCHECK=1 logs drawquad (func_001F7EF8) calls whose quad's + * GS words (+112: TEX0 at +120, TEX1 +128, ALPHA +136) are not what its + * callers put there, with the caller's return address */ +void uya_dbg_quad(RcCtx *c) +{ + static int on = -1, left = 30; + if (on < 0) on = getenv("UYA_QUADCHECK") != 0; + if (!on || left <= 0) return; + u32 a = (u32)c->r[4].d[0]; + const u32 *q = (const u32 *)RC_QA(a + 112); + if (q[4] == 0x00000260 && q[5] == 0xFF90 && q[6] == 0x44 && q[7] == 0x80) return; + left--; + uya_log("quadcheck: drawquad(%08X) from ra %08X: GS words %08X%08X %08X%08X %08X%08X %08X%08X", a, (u32)c->r[31].d[0], + q[1], q[0], q[3], q[2], q[5], q[4], q[7], q[6]); +} + +/* testing: functions named in UYA_RC_TRACE at build time call this on entry */ +void rc_trace(RcCtx *c, const char *name) +{ + /* UYA_RC_TRACE_MEM=addr,len[;addr,len...] (hex): memory shown with each call */ + static char mem[256] = "?"; + if (mem[0] == '?') { const char *e = getenv("UYA_RC_TRACE_MEM"); snprintf(mem, sizeof mem, "%s", e ? e : ""); } + char t[512]; int n = 0; + for (const char *p = mem; *p && n < 400;) { + unsigned a = 0, l = 0; + if (sscanf(p, "%x,%x", &a, &l) != 2) break; + n += snprintf(t + n, sizeof t - n, " [%X]", a); + for (unsigned i = 0; i < l && n < 480; i++) n += snprintf(t + n, sizeof t - n, "%02X", *(const u8 *)(uintptr_t)(a + i)); + while (*p && *p != ';') p++; + if (*p == ';') p++; + } + t[n] = 0; + uya_log("v%u %s(%08X, %08X, %08X, %08X) ra %08X%s", uya_frame_count(), name, (u32)c->r[4].d[0], (u32)c->r[5].d[0], + (u32)c->r[6].d[0], (u32)c->r[7].d[0], (u32)c->r[31].d[0], t); +} + +/* testing: a heuristic backtrace of translated code: the words on its stack + * (from rc_sp, which the thunks into host code set) that look like code + * addresses (return addresses the translated functions saved) */ +const char *rc_stack_scan(void) +{ + static char t[512]; + int n = 0, k = 0; + const u32 *sp = (const u32 *)(uintptr_t)rc_sp; + t[0] = 0; + for (int i = 0; sp && i < 1024 && k < 12; i += 2) { + u32 v = sp[i]; + if (v >= 0x00112380u && v < 0x00300000u && !(v & 3) && (v & 0xFFFF) && v != 0x00160000u && v != 0x00170000u) { + n += snprintf(t + n, sizeof t - n, " %08X@%d", v, i * 4); k++; + } + } + return t; +} diff --git a/games/rac1/host/src/rac1/snd989.c b/games/rac1/host/src/rac1/snd989.c new file mode 100644 index 0000000..e6e8a6b --- /dev/null +++ b/games/rac1/host/src/rac1/snd989.c @@ -0,0 +1,1667 @@ +/* 989snd's IOP side for RAC1, written for the host. + * + * The EE half of the library (src/core/989snd.c) is compiled from the decomp; + * it talks to the sound driver on the IOP through two transports, + * snd_SendIOPCommandAndWait (0x0012E688) and snd_SendIOPCommandNoWait + * (0x0012E820), whose host versions (host/overrides) hand each command to + * rac1_snd_command() here. The bank loads (0x0012E060 by disc location, + * 0x0012E1C8 from EE memory) and snd_FlushSoundCommands (0x0012DDC0) are host + * versions too. Replies that come with a callback are delivered at the next + * snd_FlushSoundCommands, the way the IOP's replies arrive one batch later. + * + * What the driver does, as 989snd does it on the IOP: + * - banks: 989snd "SBlk" version 1 sound blocks (sounds -> 0x28-byte grains, + * tones with PS-ADPCM samples), copied to host memory; + * - sounds: one handler per playing sound runs the sound's grain program at + * 240 ticks a second (tones, delays, random choices, registers, markers, + * loops, LFOs, child sounds, ...); volume, pan, pitch modulation and pitch + * bend in the library's units (0-0x400, degrees, 1/128 semitone, +-0x7FFF); + * master volume per volume group; pause/continue by group; + * - VAG streams (the game's music and dialog): VAGp files read from the disc + * as they play, started paused or not, queued after another stream (the + * music's intro then its loop), looped, paused, faded; time remaining, + * "buffered" and "still playing" replies; + * - the stream-safe CD reads (command 0x38): the game's own streamed loads, + * once VAG streaming is on, ask the IOP to read the disc into EE memory; + * - a software SPU2: 48 voices with PS-ADPCM decoding, loop flags, the + * hardware ADSR envelope, the 4-tap Gaussian interpolation and pitch in SPU + * units (0x1000 = 48 kHz), mixed at 48 kHz stereo and handed to the + * platform's audio output at every vsync (host/src/platform_*.c); + * - the SPU2's reverb (libsd's effect modes) on the voices that send to it. + * + * The grain semantics, the volume curve (MakeVolume/AdjustVolToGroup), the pan + * and pitch tables and the note-to-pitch conversion follow the jak-project's + * reimplementation of 989snd (ISC licence, see THIRD_PARTY_NOTICES.md). + * + * Not done: the noise generator (tones with flag 8), the movie sound commands + * (0x3B-0x3E, 0x5A, 0x5B; the movie player plays its own sound track). */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include +#include +#include +#include +#include "snd989_tables.h" + +#define EE(a) ((u8 *)(uintptr_t)(u32)(a)) +#define EEW(a) (*(volatile s32 *)(uintptr_t)(u32)(a)) + +static int snd_log = -1; +uint32_t uya_frame_count(void); +#define SLOG(fmt, ...) do { if (snd_log > 0) uya_log("v%u " fmt, uya_frame_count(), ##__VA_ARGS__); } while (0) + +static u32 rd32(const u8 *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((u32)p[3] << 24); } +static u16 rd16(const u8 *p) { return (u16)(p[0] | (p[1] << 8)); } +static u32 rd32be(const u8 *p) { return ((u32)p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; } +static s32 clampi(s32 v, s32 lo, s32 hi) { return v < lo ? lo : (v > hi ? hi : v); } +static s32 wrap360(s32 p) { p %= 360; return p < 0 ? p + 360 : p; } + +/* the IOP's random numbers (989snd uses the C library's rand on the IOP) */ +static u32 rng = 1; +static s32 snd_rand(void) { rng = rng * 1103515245u + 12345u; return (s32)((rng >> 16) & 0x7FFF); } + +#define VOLUME_DONT_CHANGE 0x7FFFFFFF +#define PAN_RESET (-1) +#define PAN_DONT_CHANGE (-2) + +/* ===================================================================== */ +/* SPU2 voices */ +/* ===================================================================== */ + +typedef struct { + s8 priority, vol, center_note, center_fine; + s16 pan; + s8 map_low, map_high, pb_low, pb_high; + u16 adsr1, adsr2, flags; + u32 sample; /* offset in the bank's sample data */ +} tone_t; + +struct stream; + +enum { PH_ATTACK, PH_DECAY, PH_SUSTAIN, PH_RELEASE, PH_STOPPED }; + +typedef struct { + int on; + int handler; /* sound handler index, -1 for none */ + struct stream *stream; /* the VAG stream feeding it */ + const u8 *data; /* PS-ADPCM data */ + u32 size; /* its size in bytes */ + u32 nax, lsa; /* current block, loop start (byte offsets) */ + int sub; /* next sample in the block, 28 = new block */ + int first; + u8 shift, filter, flags; + s32 h1, h2; /* ADPCM history */ + s16 hist[4]; /* last four decoded samples, oldest first */ + u32 counter; /* pitch counter (12-bit fraction) */ + u16 pitch; + /* ADSR */ + int phase, env_exp, env_dec, env_shift, env_step; + s32 level, target; + u32 env_counter; + u16 adsr1, adsr2; + s16 voll, volr; /* SPU volume registers (fixed mode) */ + int paused; + /* 989snd's view of the voice */ + tone_t tone; + u8 group; + s32 g_vol, g_pan; + s16 base_l, base_r; + s32 start_note, start_fine, cur_pm, cur_pb; + u32 age; +} voice_t; + +#define NVOICES 48 +static voice_t voices[NVOICES]; +static u32 voice_age; + +/* ---- ADSR (the SPU's envelope: one step per 48 kHz sample) ---- */ +static void adsr_settings(voice_t *v) +{ + u32 r = v->adsr1 | ((u32)v->adsr2 << 16); + switch (v->phase) { + case PH_ATTACK: + v->env_exp = (r >> 15) & 1; v->env_dec = 0; v->env_shift = (r >> 10) & 0x1F; + v->env_step = 7 - (int)((r >> 8) & 3); v->target = 0x7FFF; break; + case PH_DECAY: + v->env_exp = 1; v->env_dec = 1; v->env_shift = (r >> 4) & 0xF; v->env_step = -8; + v->target = (s32)(((r & 0xF) + 1) << 11); break; + case PH_SUSTAIN: + v->env_exp = (r >> 31) & 1; v->env_dec = (r >> 30) & 1; v->env_shift = (r >> 24) & 0x1F; + v->env_step = v->env_dec ? -8 + (int)((r >> 22) & 3) : 7 - (int)((r >> 22) & 3); v->target = 0; break; + case PH_RELEASE: + v->env_exp = (r >> 21) & 1; v->env_dec = 1; v->env_shift = (r >> 16) & 0x1F; v->env_step = -8; + v->target = 0; break; + default: break; + } +} + +static void adsr_run(voice_t *v) +{ + if (v->phase == PH_STOPPED) return; + u32 cstep = 0x800000; + int sh = v->env_shift - 11; + if (sh > 0) cstep >>= sh; + s32 step = (s16)(v->env_step << (11 - v->env_shift > 0 ? 11 - v->env_shift : 0)); + if (v->env_exp) { + if (!v->env_dec && v->level > 0x6000) cstep >>= 2; + if (v->env_dec) step = (s16)((step * v->level) >> 15); + } + v->env_counter += cstep; + if (v->env_counter >= 0x800000) { + v->env_counter = 0; + v->level = clampi(v->level + step, 0, 0x7FFF); + } + if (v->phase == PH_SUSTAIN) return; + if ((!v->env_dec && v->level >= v->target) || (v->env_dec && v->level <= v->target)) { + if (v->phase == PH_ATTACK) v->phase = PH_DECAY; + else if (v->phase == PH_DECAY) v->phase = PH_SUSTAIN; + else if (v->phase == PH_RELEASE) v->phase = PH_STOPPED; + adsr_settings(v); + } +} + +static void voice_key_off(voice_t *v) +{ + if (v->phase == PH_STOPPED) return; + v->phase = PH_RELEASE; + v->env_counter = 0; + adsr_settings(v); +} + +static void voice_kill(voice_t *v) { v->phase = PH_STOPPED; v->level = 0; } + +static const u8 *stream_block(struct stream *s, u32 off); /* below */ + +/* reads the block header at v->nax */ +static int voice_block_header(voice_t *v) +{ + const u8 *b; + if (v->stream) b = stream_block(v->stream, v->nax); + else b = v->nax + 16 <= v->size ? v->data + v->nax : 0; + if (!b) return 0; + v->shift = b[0] & 0xF; + v->filter = (b[0] >> 4) & 0x7; + v->flags = b[1]; + if (v->flags & 4) v->lsa = v->nax; + return 1; +} + +static void stream_voice_end(voice_t *v); /* below */ + +static s16 voice_next_sample(voice_t *v) +{ + static const int fk[5][2] = {{0, 0}, {60, 0}, {115, -52}, {98, -55}, {122, -60}}; + if (v->phase == PH_STOPPED) return 0; + if (v->sub >= 28) { + if (!v->first) { + if (v->stream) { + if (v->flags & 1) stream_voice_end(v); /* the end of the stream's data */ + else v->nax += 16; + if (v->phase == PH_STOPPED) return 0; + } else if (v->flags & 1) { /* loop end */ + v->nax = v->lsa; + if (!(v->flags & 2)) { voice_kill(v); return 0; } + } else v->nax += 16; + } + v->first = 0; + if (!voice_block_header(v)) { + if (v->stream) { stream_voice_end(v); if (v->phase == PH_STOPPED || !voice_block_header(v)) { voice_kill(v); return 0; } } + else { voice_kill(v); return 0; } + } + v->sub = 0; + } + const u8 *b = v->stream ? stream_block(v->stream, v->nax) : v->data + v->nax; + if (!b) { voice_kill(v); return 0; } + int nib = (b[2 + v->sub / 2] >> ((v->sub & 1) * 4)) & 0xF; + s32 s = (s16)(nib << 12); + int shift = v->shift > 12 ? 9 : v->shift; + int f = v->filter > 4 ? 0 : v->filter; + s >>= shift; + s += (fk[f][0] * v->h1) >> 6; + s += (fk[f][1] * v->h2) >> 6; + s = clampi(s, -32768, 32767); + v->h2 = v->h1; + v->h1 = s; + v->sub++; + return (s16)s; +} + +static void voice_start(voice_t *v) +{ + v->nax = 0; + v->lsa = 0; + v->sub = 28; + v->first = 1; + v->h1 = v->h2 = 0; + memset(v->hist, 0, sizeof v->hist); + v->counter = 0; + v->level = 0; + v->env_counter = 0; + v->phase = PH_ATTACK; + adsr_settings(v); + v->paused = 0; + v->age = ++voice_age; + v->on = 1; +} + +/* one output sample of a voice; returns 0 when it has nothing to say */ +static void voice_run(voice_t *v, s32 *l, s32 *r) +{ + s32 s = 0; + const short *w = snd_interp[(v->counter & 0xFF0) >> 4]; + s = (s16)(s + ((v->hist[0] * w[0]) >> 15)); + s = (s16)(s + ((v->hist[1] * w[1]) >> 15)); + s = (s16)(s + ((v->hist[2] * w[2]) >> 15)); + s = (s16)(s + ((v->hist[3] * w[3]) >> 15)); + u32 step = v->paused ? 0 : (v->pitch > 0x3FFF ? 0x3FFF : v->pitch); + v->counter += step; + u32 n = v->counter >> 12; + v->counter &= 0xFFF; + while (n--) { + v->hist[0] = v->hist[1]; v->hist[1] = v->hist[2]; v->hist[2] = v->hist[3]; + v->hist[3] = voice_next_sample(v); + } + s = (s * v->level) >> 15; + if (!v->paused) { + *l += (s * (v->voll << 1)) >> 15; + *r += (s * (v->volr << 1)) >> 15; + } + adsr_run(v); +} + +/* ===================================================================== */ +/* 989snd's volume and pitch rules */ +/* ===================================================================== */ + +static s32 master_vol[32]; +static s32 stereo_mode; /* snd_SetPlaybackMode: 1 = mono */ + +static void make_volume(int vol1, int pan1, int vol2, int pan2, int vol3, int pan3, s16 *L, s16 *R) +{ + s32 vol = vol1 * 258; + vol = vol * vol2 / 0x7F; + vol = vol * vol3 / 0x7F; + if (vol == 0) { *L = *R = 0; return; } + if (stereo_mode == 1) { *L = *R = (s16)vol; return; } + int p = wrap360(wrap360(pan1) + wrap360(pan2) + wrap360(pan3)); + if (p >= 270) p -= 270; else p += 90; + if (p < 180) { *L = (s16)(snd_pan_table[p][0] * vol / 0x3FFF); *R = (s16)(snd_pan_table[p][1] * vol / 0x3FFF); } + else { *R = (s16)(snd_pan_table[p - 180][0] * vol / 0x3FFF); *L = (s16)(snd_pan_table[p - 180][1] * vol / 0x3FFF); } +} + +static s16 adjust_vol_to_group(s16 in, int group) +{ + s32 v = in; + if (group >= 15) return (s16)v; + if (v >= 0x7FFF) v = 0x7FFE; + v = v * master_vol[group] / 0x400; + int sign = v < 0 ? -1 : 1; + return (s16)((v * v) / 0x7FFE * sign); +} + +static u16 note2pitch(u16 center_note, u16 center_fine, u16 note, short fine) +{ + s32 _fine = fine + (u16)center_fine, _fine2 = _fine, _note, offset1, offset2, val, val2, val3, ret; + if (_fine < 0) _fine2 = _fine + 127; + _fine2 = _fine2 / 128; + _note = note + _fine2 - center_note; + val3 = _note / 6; + if (_note < 0) val3--; + offset2 = _fine - _fine2 * 128; + val2 = _note < 0 ? -1 : 0; + if (val3 < 0) val3--; + val2 = (val3 / 2) - val2; + val = val2 - 2; + offset1 = _note - (val2 * 12); + if ((offset1 < 0) || ((offset1 == 0) && (offset2 < 0))) { offset1 = offset1 + 12; val = val2 - 3; } + if (offset2 < 0) { offset1 = (offset1 - 1) + _fine2; offset2 += (_fine2 + 1) * 128; } + if (offset1 < 0) offset1 = 0; + if (offset1 > 11) offset1 = 11; + if (offset2 < 0) offset2 = 0; + if (offset2 > 127) offset2 = 127; + ret = (snd_note_pitch[offset1] * snd_note_pitch[offset2 + 12]) / 0x10000; + if (val < 0) ret = (ret + (1 << (-val - 1))) >> -val; + return (u16)ret; +} + +static u16 ps1_note2pitch(s8 center_note, s8 center_fine, short note, short fine) +{ + int ps1 = center_note >= 0; + if (!ps1) center_note = -center_note; + u16 p = note2pitch((u16)center_note, (u16)(u8)center_fine, (u16)note, fine); + if (ps1) p = (u16)(44100 * p / 48000); + return p; +} + +static void pitch_bend(const tone_t *t, int pb, int pm, int start_note, int start_fine, s16 *note, s16 *fine) +{ + s32 v9 = (start_note << 7) + start_fine + pm, v7; + if (pb >= 0) v7 = t->pb_high * (pb << 7) / 0x7FFF + v9; + else v7 = t->pb_low * (pb << 7) / 0x8000 + v9; + *note = (s16)(v7 / 128); + *fine = (s16)(v7 % 128); +} + +static void voice_apply_volume(voice_t *v) +{ + v->voll = (s16)(adjust_vol_to_group(v->base_l, v->group) >> 1); + v->volr = (s16)(adjust_vol_to_group(v->base_r, v->group) >> 1); +} + +static void voice_apply_pitch(voice_t *v) +{ + s16 note, fine; + pitch_bend(&v->tone, v->cur_pb, v->cur_pm, v->start_note, v->start_fine, ¬e, &fine); + v->pitch = ps1_note2pitch(v->tone.center_note, v->tone.center_fine, note, fine); +} + +/* ===================================================================== */ +/* banks */ +/* ===================================================================== */ + +typedef struct { + int used; + u32 handle; + u8 *data; + u32 size; + u8 *sblk; /* the SBlk chunk */ + u32 sblk_size; + const u8 *smp; /* sample data */ + u32 smp_size; + int nsounds; + u32 first_sound, first_grain; +} bank_t; + +#define NBANKS 16 +static bank_t banks[NBANKS]; +static u32 bank_serial; + +static u32 bank_file_size(const u8 *b) +{ + u32 n = rd32(b + 4), end = 0; + if (n == 0 || n > 4) return 0; + for (u32 i = 0; i < n; i++) { + u32 e = rd32(b + 8 + i * 8) + rd32(b + 12 + i * 8); + if (e > end) end = e; + } + return end; +} + +static u32 bank_add(const u8 *src, u32 size) +{ + if (size < 0x18 || size > (32u << 20)) { uya_log("989snd: bad bank size %u", size); return 0; } + u32 so = rd32(src + 8), ss = rd32(src + 12); + if (so + 0x40 > size || memcmp(src + so, "SBlk", 4)) { uya_log("989snd: not a 989snd bank"); return 0; } + for (int i = 0; i < NBANKS; i++) { + bank_t *b = &banks[i]; + if (b->used) continue; + u8 *d = malloc(size); + if (!d) return 0; + memcpy(d, src, size); + memset(b, 0, sizeof *b); + b->used = 1; + b->data = d; + b->size = size; + b->sblk = d + so; + b->sblk_size = ss; + if (rd32(src + 4) >= 2) { b->smp = d + rd32(src + 16); b->smp_size = rd32(src + 20); } + if (b->smp && b->smp + b->smp_size > d + size) b->smp_size = (u32)(d + size - b->smp); + b->nsounds = (s16)rd16(b->sblk + 0x16); + b->first_sound = rd32(b->sblk + 0x1C); + b->first_grain = rd32(b->sblk + 0x20); + b->handle = 0x00120000u + (u32)i * 0x1000u + ((++bank_serial) & 0xFFF); + if (rd32(b->sblk + 4) != 1) uya_log("989snd: SBlk version %u (only 1 is expected)", rd32(b->sblk + 4)); + uya_log("989snd: bank %08X loaded: %d sounds, %u KB of samples", b->handle, b->nsounds, b->smp_size / 1024); + if (snd_log > 0 && getenv("UYA_DUMP")) { /* for checking: the bank as loaded */ + char path[512]; + snprintf(path, sizeof path, "%s/bank_%08X.bin", getenv("UYA_DUMP"), b->handle); + FILE *f = fopen(path, "wb"); + if (f) { fwrite(d, 1, size, f); fclose(f); } + } + return b->handle; + } + uya_log("989snd: no room for another bank"); + return 0; +} + +static bank_t *bank_get(u32 h) +{ + for (int i = 0; i < NBANKS; i++) if (banks[i].used && banks[i].handle == h) return &banks[i]; + return 0; +} + +static const u8 *sfx_entry(const bank_t *b, int sfx) { return b->sblk + b->first_sound + sfx * 12; } +static const u8 *grain_at(const bank_t *b, int sfx, int g) { return b->sblk + b->first_grain + rd32(sfx_entry(b, sfx) + 8) + g * 0x28; } +static int sfx_ngrains(const bank_t *b, int sfx) { return (s8)sfx_entry(b, sfx)[4]; } + +static void read_tone(const u8 *p, tone_t *t) +{ + t->priority = (s8)p[0]; t->vol = (s8)p[1]; t->center_note = (s8)p[2]; t->center_fine = (s8)p[3]; + t->pan = (s16)rd16(p + 4); + t->map_low = (s8)p[6]; t->map_high = (s8)p[7]; t->pb_low = (s8)p[8]; t->pb_high = (s8)p[9]; + t->adsr1 = rd16(p + 0xA); t->adsr2 = rd16(p + 0xC); t->flags = rd16(p + 0xE); + t->sample = rd32(p + 0x10); +} + +/* ===================================================================== */ +/* sound handlers (989snd's "block sound" player) */ +/* ===================================================================== */ + +typedef struct { + int type, target, target_extra, setup_flags; + s32 depth, next_step, step_size, state_hold1, state_hold2, range, last_lfo; + u32 tick; +} lfo_t; + +enum { LFO_OFF, LFO_SINE, LFO_SQUARE, LFO_TRIANGLE, LFO_SAW, LFO_RAND }; +enum { LT_NONE, LT_VOLUME, LT_PAN, LT_PMOD, LT_PBEND }; + +typedef struct { + int used; + u32 handle; /* the handle the EE knows (shared by children) */ + int parent; /* parent handler index, -1 for a top-level sound */ + bank_t *bank; + int sfx, orig_sfx; + u8 group; + int done, paused; + int next_grain, countdown; + int grains_to_play, grains_to_skip, skip_grains; + s32 orig_volume, orig_pan, cur_volume, cur_pan, cur_pm, cur_pb; + s32 app_volume, app_pan, app_pm, app_pb; + s32 lfo_volume, lfo_pan, lfo_pm, lfo_pb; + int note, fine; + s8 regs[4]; + lfo_t lfo[4]; + u32 start_tick; +} handler_t; + +#define NHANDLERS 160 +static handler_t handlers[NHANDLERS]; +static s8 g_block_reg[32]; +static u32 snd_tick; +static u32 next_handle = 1; + +static u32 new_handle(void) +{ + u32 h = next_handle++; + if (next_handle >= 0x7FFFFFF0u) next_handle = 1; + return h; +} + +static int handler_has_voices(int hi) +{ + for (int i = 0; i < NVOICES; i++) if (voices[i].on && voices[i].handler == hi) return 1; + return 0; +} + +static int handler_has_children(int hi) +{ + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent == hi) return 1; + return 0; +} + +static void handler_free(int hi) +{ + for (int i = 0; i < NHANDLERS; i++) + if (handlers[i].used && handlers[i].parent == hi) handler_free(i); + for (int i = 0; i < NVOICES; i++) + if (voices[i].on && voices[i].handler == hi) { voice_kill(&voices[i]); voices[i].handler = -1; } + handlers[hi].used = 0; +} + +static void handler_set_vol_pan(int hi, s32 vol, s32 pan); +static void handler_update_pitch(int hi); + +static s32 sine_lfo(int step) { return (s32)floor(32767.0 * sin((double)step * 6.283185307179586 / 2048.0) + 0.5); } /* not lrint: the game thread truncates */ + +static s32 lfo_get(lfo_t *l, s32 mult) +{ + s32 step = l->next_step >> 16, ret = 0; + l->next_step += mult * l->step_size; + if (l->next_step > 0x7FFFFFF) l->next_step -= 0x8000000; + step &= 2047; + switch (l->type) { + case LFO_SINE: ret = sine_lfo(step); break; + case LFO_SQUARE: ret = step >= l->state_hold1 ? -32767 : 32767; break; + case LFO_TRIANGLE: + if (step < 512) ret = 0x7FFF * step / 512; + else if (step >= 1536) ret = 0x7FFF * (step - 1536) / 512 - 0x7FFF; + else ret = 0x7FFF - 65534 * (step - 512) / 1024; + break; + case LFO_SAW: ret = step >= 1024 ? 0x7FFF * (step - 1024) / 1024 - 0x7FFF : 0x7FFF * step / 1023; break; + case LFO_RAND: + if (step >= 1024 && l->state_hold2 == 1) { l->state_hold2 = 0; l->state_hold1 = 2 * ((snd_rand() & 0x7FFF) - 0x3FFF); } + else if (step < 1024 && l->state_hold2 == 0) { l->state_hold2 = 1; l->state_hold1 = -(snd_rand() & 0x7FFF) * (snd_rand() & 1); } + ret = l->state_hold1; + break; + default: ret = 0; break; + } + if (l->setup_flags & 1) ret = -ret; + return ret; +} + +static void lfo_calc_depth(int hi, lfo_t *l) +{ + handler_t *h = &handlers[hi]; + if (l->target == LT_VOLUME) l->range = ((s8)sfx_entry(h->bank, h->sfx)[0] * l->depth) >> 10; + if (l->target == LT_PAN) l->range = (180 * l->depth) >> 10; + if (l->target == LT_PMOD) l->range = (6096 * l->depth) >> 10; + if (l->target == LT_PBEND) l->range = (0x7FFF * l->depth) >> 10; + l->last_lfo = 0; +} + +static void lfo_tick(int hi, lfo_t *l) +{ + handler_t *h = &handlers[hi]; + l->tick++; + if (l->target == LT_NONE || l->type == LFO_OFF || (l->tick & 1) == 0) return; + switch (l->target) { + case LT_VOLUME: { + s32 v = (l->range * (lfo_get(l, 2) - 0x7FFF)) >> 16; + if (h->lfo_volume != v) { h->lfo_volume = v; handler_set_vol_pan(hi, VOLUME_DONT_CHANGE, PAN_DONT_CHANGE); } + } break; + case LT_PAN: { + s32 p = (l->range * lfo_get(l, 2)) >> 15; + if (h->lfo_pan != p) { h->lfo_pan = p; handler_set_vol_pan(hi, VOLUME_DONT_CHANGE, PAN_DONT_CHANGE); } + } break; + case LT_PMOD: { + s32 pm = (lfo_get(l, 2) * l->range) >> 15; + if (h->lfo_pm != pm) { h->lfo_pm = pm; handler_update_pitch(hi); } + } break; + case LT_PBEND: { + s32 pb = (lfo_get(l, 2) * l->range) >> 15; + if (h->lfo_pb != pb) { h->lfo_pb = pb; handler_update_pitch(hi); } + } break; + default: break; + } +} + +static void handler_set_vol_pan(int hi, s32 vol, s32 pan) +{ + handler_t *h = &handlers[hi]; + if (vol >= 0) { if (vol != VOLUME_DONT_CHANGE) h->app_volume = vol; } + else h->app_volume = -1024 * vol / 127; + if (pan == PAN_RESET) h->app_pan = (s16)rd16(sfx_entry(h->bank, h->sfx) + 2); + else if (pan != PAN_DONT_CHANGE) h->app_pan = pan; + s32 nv = clampi(((h->app_volume * h->orig_volume) >> 10) + h->lfo_volume, 0, 127); + s32 np = wrap360(h->app_pan % 360 + h->lfo_pan); + if (np != h->cur_pan || nv != h->cur_volume) { + h->cur_volume = nv; + h->cur_pan = np; + for (int i = 0; i < NHANDLERS; i++) + if (handlers[i].used && handlers[i].parent == hi) handler_set_vol_pan(i, h->app_volume * h->orig_volume / 127, pan); + for (int i = 0; i < NVOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on || v->handler != hi) continue; + make_volume(127, 0, h->cur_volume, h->cur_pan, v->g_vol, v->g_pan, &v->base_l, &v->base_r); + voice_apply_volume(v); + } + } +} + +static void handler_update_pitch(int hi) +{ + handler_t *h = &handlers[hi]; + h->cur_pm = h->app_pm + h->lfo_pm; + h->cur_pb = clampi(h->app_pb + h->lfo_pb, -32768, 32767); + for (int i = 0; i < NVOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on || v->handler != hi) continue; + v->cur_pm = h->cur_pm; + v->cur_pb = h->cur_pb; + voice_apply_pitch(v); + } +} + +static void handler_set_pm(int hi, s32 pm) +{ + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent == hi) handler_set_pm(i, pm); + handlers[hi].app_pm = pm; + handler_update_pitch(hi); +} + +static void handler_set_pb(int hi, s32 pb) +{ + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent == hi) handler_set_pb(i, pb); + handlers[hi].app_pb = pb; + handler_update_pitch(hi); +} + +static void handler_stop(int hi) +{ + handlers[hi].done = 1; + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent == hi) handler_stop(i); + for (int i = 0; i < NVOICES; i++) if (voices[i].on && voices[i].handler == hi) voice_key_off(&voices[i]); +} + +static void handler_pause(int hi, int pause) +{ + handlers[hi].paused = pause; + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent == hi) handler_pause(i, pause); + for (int i = 0; i < NVOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on || v->handler != hi) continue; + v->paused = pause; + if (!pause) { voice_apply_volume(v); voice_apply_pitch(v); } + } +} + +/* a free SPU voice, or the one to steal (released first, then the lowest priority, then the oldest) */ +static voice_t *voice_alloc(int priority) +{ + voice_t *best = 0; + for (int i = 0; i < NVOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on) return v; + if (v->stream) continue; + if (!best) { best = v; continue; } + int vr = v->phase >= PH_RELEASE, br = best->phase >= PH_RELEASE; + if (vr != br) { if (vr) best = v; continue; } + if (v->tone.priority != best->tone.priority) { if (v->tone.priority < best->tone.priority) best = v; continue; } + if (v->age < best->age) best = v; + } + if (best && best->phase < PH_RELEASE && best->tone.priority > priority) return 0; + return best; +} + +static s32 reg_value(handler_t *h, s32 x, int pan) +{ + if (x >= 0) return x; + s32 r; + if (x >= -4) r = h->regs[-x - 1]; + else if (x == -5) return pan ? snd_rand() % 360 : snd_rand() % 0x7F; + else r = g_block_reg[(-x - 6) & 31]; + return pan ? 360 * r / 127 : r; +} + +static void grain_tone(int hi, const u8 *gp) +{ + handler_t *h = &handlers[hi]; + tone_t t; + read_tone(gp + 8, &t); + h->cur_volume = clampi(((h->app_volume * h->orig_volume) >> 10) + h->lfo_volume, 0, 127); + h->cur_pan = wrap360(h->app_pan % 360 + h->lfo_pan); + if (t.flags & 8) return; /* noise: not done */ + if (!h->bank->smp || t.sample >= h->bank->smp_size) return; + s32 vol = t.vol; + if (vol < 0) vol = reg_value(h, vol, 0); + if (vol < 0) vol = 0; + s32 pan = t.pan; + if (pan < 0) { + if (pan >= -4) pan = 360 * h->regs[-pan - 1] / 127; + else if (pan == -5) pan = snd_rand() % 360; + else pan = 360 * g_block_reg[(-pan - 6) & 31] / 127; + } + pan = wrap360(pan); + voice_t *v = voice_alloc(t.priority); + if (!v) return; + if (v->on && v->handler >= 0) { /* stolen */ } + memset(v, 0, sizeof *v); + v->handler = hi; + v->data = h->bank->smp + t.sample; + v->size = h->bank->smp_size - t.sample; + v->tone = t; + v->group = h->group; + v->g_vol = vol; + v->g_pan = pan; + v->start_note = h->note; + v->start_fine = h->fine; + v->cur_pb = h->cur_pb; + v->cur_pm = h->cur_pm; + v->adsr1 = t.adsr1; + v->adsr2 = t.adsr2; + make_volume(127, 0, h->cur_volume, h->cur_pan, vol, pan, &v->base_l, &v->base_r); + voice_apply_volume(v); + voice_apply_pitch(v); + voice_start(v); + if (h->paused) v->paused = 1; +} + +static int handler_new(bank_t *b, int sfx, s32 sfx_vol, s32 sfx_pan, s32 vol, s32 pan, s32 pm, s32 pb, + const s8 *regs, int parent, u32 handle); + +/* runs one grain; returns extra delay ticks */ +static int do_grain_op(int hi) +{ + handler_t *h = &handlers[hi]; + bank_t *b = h->bank; + int ng = sfx_ngrains(b, h->sfx); + u8 *gp = (u8 *)grain_at(b, h->sfx, h->next_grain); + u32 type = rd32(gp); + s16 p0 = (s16)rd16(gp + 8), p1 = (s16)rd16(gp + 10), p2 = (s16)rd16(gp + 12); + switch (type) { + case 1: case 9: grain_tone(hi, gp); return 0; + case 4: { /* LFO settings */ + int which = gp[8] & 3; + lfo_t *l = &h->lfo[which]; + l->target = gp[9]; + if (l->target != LT_NONE) { + l->type = gp[11]; + l->target_extra = gp[10]; + l->setup_flags = rd16(gp + 16); + l->depth = rd16(gp + 14); + l->step_size = (s32)rd32(gp + 20); + l->state_hold1 = 0; + l->last_lfo = 0; + if (l->type == LFO_SQUARE) l->state_hold1 = rd16(gp + 12); + l->state_hold2 = 0; + if (l->setup_flags & 2) l->next_step = (snd_rand() & 0x7FF) << 16; + else l->next_step = rd16(gp + 18) << 16; + if (l->type == LFO_RAND) { l->state_hold1 = -(snd_rand() & 0x7FFF) * (snd_rand() & 1); l->state_hold2 = 1; } + lfo_calc_depth(hi, l); + lfo_tick(hi, l); + } else l->type = LFO_OFF; + return 0; + } + case 5: { /* start child sound */ + s32 vol = (s32)rd32(gp + 8), pan = (s32)rd32(gp + 12), id = (s32)rd32(gp + 20); + if (vol < 0) vol = reg_value(h, vol, 0); + vol = clampi(abs(vol), 0, 127); + if (pan < 0) { + if (pan >= -4) pan = 360 * (abs(h->regs[-pan - 1]) > 127 ? 127 : abs(h->regs[-pan - 1])) / 127; + else if (pan == -5) pan = snd_rand() % 360; + else { int r = abs(g_block_reg[(-pan - 6) & 31]); pan = 360 * (r > 127 ? 127 : r) / 127; } + } + if (id >= 0 && id < b->nsounds) + handler_new(b, id, vol, pan, h->app_volume * h->orig_volume / 127, h->app_pan, h->app_pm, h->app_pb, + h->regs, hi, h->handle); + return 0; + } + case 6: { /* stop child sound */ + s32 id = (s32)rd32(gp + 20); + if (id >= 0) + for (int i = 0; i < NHANDLERS; i++) + if (handlers[i].used && handlers[i].parent == hi && handlers[i].sfx == id) handler_free(i); + return 0; + } + case 8: { /* branch */ + s32 id = (s32)rd32(gp + 20); + if (id < 0 || id >= b->nsounds) return 0; + for (int i = 0; i < NVOICES; i++) + if (voices[i].on && voices[i].handler == hi) { voice_key_off(&voices[i]); voices[i].voll = voices[i].volr = 0; } + const u8 *e = sfx_entry(b, id); + h->sfx = id; + h->orig_volume = (s8)e[0]; + h->orig_pan = (s16)rd16(e + 2); + h->cur_volume = (h->app_volume * (s8)e[0]) >> 10; + h->group = (u8)e[1]; + h->countdown = sfx_ngrains(b, id) ? (s32)rd32(grain_at(b, id, 0) + 4) : 0; + h->next_grain = -1; + h->skip_grains = 0; + h->grains_to_play = 0; + h->grains_to_skip = 0; + return 0; + } + case 21: case 20: case 35: case 0: return 0; /* loop start, control null, marker, null */ + case 22: { /* loop end: back to the loop start */ + for (int i = h->next_grain - 1; i >= 0; i--) + if (rd32(grain_at(b, h->sfx, i)) == 21) { h->next_grain = i - 1; break; } + return 0; + } + case 23: { /* loop continue: past the loop end */ + for (int i = h->next_grain + 1; i < ng; i++) + if (rd32(grain_at(b, h->sfx, i)) == 22) { h->next_grain = i; break; } + return 0; + } + case 24: h->done = 1; return 0; /* stop */ + case 25: { /* random play: one of p0 groups of p1 grains */ + int options = p0, count = p1; + if (options <= 0) return 0; + int rnd = snd_rand() % options; + if (rnd == p2) { rnd++; if (rnd >= options) rnd = 0; } + gp[12] = (u8)rnd; gp[13] = (u8)(rnd >> 8); + h->next_grain += rnd * count; + h->grains_to_play = count + 1; + h->grains_to_skip = (options - 1 - rnd) * count; + h->skip_grains = 1; + return 0; + } + case 26: { /* random delay */ + s32 amount = (s32)rd32(gp + 8); + return amount > 0 ? snd_rand() % amount : 0; + } + case 27: { /* random pitch bend */ + s32 r = snd_rand(); + handler_set_pb(hi, p0 * ((0xFFFF * (r % 0x7FFF)) / 0x7FFF - 0x8000) / 100); + return 0; + } + case 28: /* pitch bend */ + handler_set_pb(hi, p0 >= 0 ? 0x7FFF * p0 / 127 : -0x8000 * p0 / -128); + return 0; + case 29: /* add to the pitch bend */ + handler_set_pb(hi, clampi(h->cur_pb + 0x7FFF * p0 / 127, -32768, 32767)); + return 0; + case 30: /* set register */ + if (p0 < 0) g_block_reg[(-p0 - 1) & 31] = (s8)p1; else h->regs[p0 & 3] = (s8)p1; + return 0; + case 31: { /* set register to a random value */ + s32 range = p2 - p1 + 1; + s32 v = (range > 0 ? snd_rand() % range : 0) + p1; + if (p0 < 0) g_block_reg[(-p0 - 1) & 31] = (s8)v; else h->regs[p0 & 3] = (s8)v; + return 0; + } + case 32: case 33: { /* increment / decrement a register */ + s8 *r = p0 < 0 ? &g_block_reg[(-p0 - 1) & 31] : &h->regs[p0 & 3]; + *r = (s8)clampi(*r + (type == 32 ? 1 : -1), -128, 127); + return 0; + } + case 34: { /* test a register: skip the next grain */ + s32 v = p0 < 0 ? g_block_reg[(-p0 - 1) & 31] : h->regs[p0 & 3]; + if (p1 == 0) { if (v >= p2) h->next_grain++; } + else if (p1 == 1) { if (v != p2) h->next_grain++; } + else if (p2 >= v) h->next_grain++; + return 0; + } + case 36: case 37: { /* go to a (random) marker */ + s32 mark = p0; + if (type == 37) { s32 range = p1 - p0 + 1; mark = (range > 0 ? snd_rand() % range : 0) + p0; } + for (int i = 0; i < ng; i++) { + const u8 *g = grain_at(b, h->sfx, i); + if (rd32(g) == 35 && (s16)rd16(g + 8) == mark) { h->next_grain = i - 1; break; } + } + return 0; + } + case 38: /* wait for all voices */ + if (handler_has_voices(hi)) { h->next_grain--; return 1; } + return 0; + case 39: { /* play cycle */ + int a = p2, idx = p2 + 1; + if (idx == p0) idx = 0; + gp[12] = (u8)idx; gp[13] = (u8)(idx >> 8); + h->next_grain += p1 * a; + h->grains_to_play = p1 + 1; + h->grains_to_skip = (p0 - 1 - a) * p1; + h->skip_grains = 1; + return 0; + } + case 40: { /* add to a register */ + s8 *r = p1 < 0 ? &g_block_reg[(-p1 - 1) & 31] : &h->regs[p1 & 3]; + *r = (s8)clampi(*r + p0, -128, 127); + return 0; + } + case 41: case 42: /* key off / kill the voices */ + for (int i = 0; i < NVOICES; i++) + if (voices[i].on && voices[i].handler == hi) { + voice_key_off(&voices[i]); + if (type == 42) voices[i].voll = voices[i].volr = 0; + } + return 0; + case 43: h->next_grain = ng - 1; return 0; /* on-stop marker */ + case 44: { /* copy a register */ + s8 v = p0 < 0 ? g_block_reg[(-p0 - 1) & 31] : h->regs[p0 & 3]; + if (p1 < 0) g_block_reg[(-p1 - 1) & 31] = v; else h->regs[p1 & 3] = v; + return 0; + } + case 7: /* plugin message: none on RAC1's IOP */ + default: + return 0; + } +} + +static void do_grain(int hi) +{ + handler_t *h = &handlers[hi]; + int ret = do_grain_op(hi); + if (h->skip_grains) { + h->grains_to_play--; + if (h->grains_to_play == 0) { h->next_grain += h->grains_to_skip; h->skip_grains = 0; } + } + h->next_grain++; + if (h->next_grain >= sfx_ngrains(h->bank, h->sfx) || h->next_grain < 0) { h->done = 1; return; } + h->countdown = (s32)rd32(grain_at(h->bank, h->sfx, h->next_grain) + 4) + ret; +} + +/* 989snd's instance limit: returns the handler to stop, -2 for "the new one", -1 for none */ +static int check_instance_limit(int hi, s32 vol) +{ + handler_t *h = &handlers[hi]; + const u8 *e = sfx_entry(h->bank, h->sfx); + int limit = (s8)e[5], flags = rd16(e + 6); + if (!limit || !(flags & 8)) return -1; + int weakest = -1, inst = 0; + for (int i = 0; i < NHANDLERS; i++) { + handler_t *o = &handlers[i]; + if (!o->used || i == hi || o->parent >= 0 || o->done) continue; + if (o->bank == h->bank && (o->orig_sfx == h->sfx || o->sfx == h->sfx)) { + inst++; + if (weakest < 0 || ((flags & 0x10) && o->app_volume < handlers[weakest].app_volume) || + ((flags & 0x20) && o->start_tick < handlers[weakest].start_tick)) + weakest = i; + } + } + if (limit - 1 < inst) { + if (weakest >= 0 && (((flags & 0x10) && handlers[weakest].app_volume < vol) || (flags & 0x20))) return weakest; + return -2; + } + return -1; +} + +static int handler_new(bank_t *b, int sfx, s32 sfx_vol, s32 sfx_pan, s32 vol, s32 pan, s32 pm, s32 pb, + const s8 *regs, int parent, u32 handle) +{ + if (sfx < 0 || sfx >= b->nsounds || sfx_ngrains(b, sfx) <= 0) return -1; + int hi; + for (hi = 0; hi < NHANDLERS && handlers[hi].used; hi++) {} + if (hi == NHANDLERS) { SLOG("989snd: out of sound handlers"); return -1; } + const u8 *e = sfx_entry(b, sfx); + handler_t *h = &handlers[hi]; + memset(h, 0, sizeof *h); + h->used = 1; + h->handle = handle; + h->parent = parent; + h->bank = b; + h->sfx = h->orig_sfx = sfx; + h->group = (u8)e[1]; + h->note = 60; + h->fine = 0; + h->start_tick = snd_tick; + if (sfx_vol == -1) sfx_vol = (s8)e[0]; + if (sfx_pan == -1) sfx_pan = (s16)rd16(e + 2); + if (vol == VOLUME_DONT_CHANGE) vol = 1024; + s32 play_vol = (sfx_vol * vol) >> 10; + if (play_vol >= 128) play_vol = 127; + if (pan == PAN_RESET || pan == PAN_DONT_CHANGE) pan = sfx_pan; + h->orig_volume = sfx_vol; + h->orig_pan = sfx_pan; + h->cur_volume = play_vol; + h->cur_pan = pan; + h->cur_pb = pb; + h->cur_pm = pm; + h->app_volume = vol; + h->app_pan = pan; + h->app_pb = pb; + h->app_pm = pm; + if (regs) memcpy(h->regs, regs, 4); + if (parent < 0) { + int stop = check_instance_limit(hi, vol); + if (stop == -2) { h->used = 0; return -1; } + if (stop >= 0) handler_stop(stop); + } + h->next_grain = 0; + h->countdown = (s32)rd32(grain_at(b, sfx, 0) + 4); + while (h->countdown <= 0 && !h->done) do_grain(hi); + return hi; +} + +/* one 240 Hz tick of a handler; returns 1 when it is finished */ +static int handler_tick(int hi) +{ + handler_t *h = &handlers[hi]; + for (int i = 0; i < 4; i++) lfo_tick(hi, &h->lfo[i]); + if (h->done && !handler_has_children(hi)) return !handler_has_voices(hi); + if (h->paused || h->done) return 0; + h->countdown--; + while (h->countdown <= 0 && !h->done) do_grain(hi); + return 0; +} + +static int find_handler(u32 handle) +{ + if (!handle) return -1; + for (int i = 0; i < NHANDLERS; i++) + if (handlers[i].used && handlers[i].parent < 0 && handlers[i].handle == handle) return i; + return -1; +} + +/* ===================================================================== */ +/* VAG streams */ +/* ===================================================================== */ + +typedef struct stream { + int used; + u32 handle; + u32 lsn; /* the VAGp file on the disc */ + u32 size; /* data bytes after the 0x30-byte header */ + u32 rate; + u8 *buf; /* the file's data, read as it plays */ + u32 loaded; /* bytes of buf read so far */ + s32 vol, pan, group, flags; + int paused; + int voice; /* index in voices[] */ + struct { int on; u32 lsn; s32 vol, pan, flags; } next; /* queued after this one */ +} stream_t; + +#define NSTREAMS 8 +#define STREAM_CHUNK 32 /* sectors read at a time */ +static stream_t streams[NSTREAMS]; + +static int stream_open_file(stream_t *s, u32 lsn) +{ + u8 head[2048]; + if (uya_disc_read(lsn, 1, head) != 0 || memcmp(head, "VAGp", 4)) { + uya_log("989snd: no VAG stream at LSN %u", lsn); + return 0; + } + u32 size = rd32be(head + 12), rate = rd32be(head + 16); + if (size == 0 || size > (256u << 20)) return 0; + free(s->buf); + u32 total = 0x30 + size; + s->buf = malloc(((total + 2047) / 2048 + 1) * 2048); + if (!s->buf) return 0; + memcpy(s->buf, head, 2048); + s->loaded = 2048; + s->lsn = lsn; + s->size = size; + s->rate = rate ? rate : 44100; + return 1; +} + +/* the 16-byte ADPCM block at data offset off, reading the disc as needed */ +static const u8 *stream_block(stream_t *s, u32 off) +{ + if (!s->buf || off + 16 > s->size) return 0; + u32 need = 0x30 + off + 16; + while (s->loaded < need) { + u32 sec = s->loaded / 2048, total = (0x30 + s->size + 2047) / 2048, n = total - sec; + if (n > STREAM_CHUNK) n = STREAM_CHUNK; + if (n == 0 || uya_disc_read(s->lsn + sec, n, s->buf + s->loaded) != 0) return 0; + s->loaded += n * 2048; + } + return s->buf + 0x30 + off; +} + +static void stream_set_volume(stream_t *s) +{ + voice_t *v = &voices[s->voice]; + s32 vol = clampi(s->vol, 0, 0x400) * 127 / 0x400; + make_volume(127, 0, vol, s->pan, 127, 0, &v->base_l, &v->base_r); + v->group = (u8)s->group; + voice_apply_volume(v); +} + +static void stream_free(stream_t *s) +{ + if (s->voice >= 0 && voices[s->voice].stream == s) { voices[s->voice].on = 0; voices[s->voice].stream = 0; } + free(s->buf); + memset(s, 0, sizeof *s); + s->voice = -1; +} + +/* the voice reached the end of the stream's data */ +static void stream_voice_end(voice_t *v) +{ + stream_t *s = v->stream; + if (s->next.on) { + u32 lsn = s->next.lsn; + s->vol = s->next.vol; s->pan = s->next.pan; s->flags = s->next.flags; + s->next.on = 0; + if (stream_open_file(s, lsn)) { + SLOG("989snd: stream %08X continues with LSN %u", s->handle, lsn); + v->nax = 0; v->first = 1; v->sub = 28; v->flags = 0; + v->pitch = (u16)((u64)s->rate * 4096 / 48000); + stream_set_volume(s); + return; + } + } + if (s->flags & 4) { v->nax = 0; v->flags = 0; return; } /* looped */ + voice_kill(v); +} + +static stream_t *find_stream(u32 handle) +{ + if (!handle) return 0; + for (int i = 0; i < NSTREAMS; i++) if (streams[i].used && streams[i].handle == handle) return &streams[i]; + return 0; +} + +static u32 stream_play(const s32 *a) +{ + u32 lsn = (u32)a[0], queue = (u32)a[5]; + s32 vol = a[2] >> 16, pan = a[3] >> 16, group = a[4], flags = a[6]; + SLOG("989snd: VAG stream LSN %u (LSN2 %u) vol %d pan %d group %d queue %08X flags %X", lsn, (u32)a[1], vol, pan, group, queue, flags); + stream_t *q = find_stream(queue); + if (q) { /* plays when that one ends, same handle */ + q->next.on = 1; q->next.lsn = lsn; q->next.vol = vol; q->next.pan = pan; q->next.flags = flags; + return q->handle; + } + int si, vi; + for (si = 0; si < NSTREAMS && streams[si].used; si++) {} + if (si == NSTREAMS) return 0; + for (vi = NVOICES - 1; vi >= 0 && voices[vi].on; vi--) {} + if (vi < 0) { + voice_t *v = voice_alloc(127); + if (!v) return 0; + vi = (int)(v - voices); + } + stream_t *s = &streams[si]; + memset(s, 0, sizeof *s); + s->voice = -1; + if (!stream_open_file(s, lsn)) { free(s->buf); s->buf = 0; return 0; } + s->used = 1; + s->handle = new_handle(); + s->vol = vol; s->pan = pan; s->group = group; s->flags = flags; + s->voice = vi; + voice_t *v = &voices[vi]; + memset(v, 0, sizeof *v); + v->handler = -1; + v->stream = s; + v->size = s->size; + v->adsr1 = 0x00FF; /* attack at once, held */ + v->adsr2 = 0x1FC0; + v->pitch = (u16)((u64)s->rate * 4096 / 48000); + voice_start(v); + v->level = 0x7FFF; + v->phase = PH_SUSTAIN; + adsr_settings(v); + stream_set_volume(s); + s->paused = v->paused = (flags & 1) != 0; /* started paused: continued by command 0x2E */ + return s->handle; +} + +static int stream_active(stream_t *s) { return s && s->voice >= 0 && voices[s->voice].on && voices[s->voice].stream == s; } + +/* ===================================================================== */ +/* the mixer */ +/* ===================================================================== */ + +static int htick = 200; + +static void tick_all(void) +{ + snd_tick++; + for (int i = 0; i < NHANDLERS; i++) { + if (!handlers[i].used) continue; + if (handler_tick(i)) handler_free(i); + } + for (int i = 0; i < NSTREAMS; i++) + if (streams[i].used && !stream_active(&streams[i])) { + SLOG("989snd: stream %08X ended", streams[i].handle); + stream_free(&streams[i]); + } +} + +/* ---- the SPU2's reverb ---- + * The hardware's effect: a work area of 16-bit samples running at half the + * output rate, with same-side and cross reflections, four comb taps and two + * all-pass filters (the PS1/PS2 formula as documented in nocash's psx-spx). + * The presets are libsd's effect modes (sceSdSetEffectAttr: room, studio + * A/B/C, hall, space, echo, delay, pipe), whose register values are the PS1 + * library's: address registers in 8-byte units, sizes in bytes. For echo and + * delay the delay and feedback parameters recompute some of them. Voices whose + * tone has flag 1 (989snd's "reverb" output) feed it; 989snd command 0x50 sets + * the mode, depth (the return volume, 0-0x7FFF), delay and feedback, and 0x10 + * fades the depth. */ +typedef struct { + u32 size; + s16 dAPF1, dAPF2, vIIR, vCOMB1, vCOMB2, vCOMB3, vCOMB4, vWALL; + s16 vAPF1, vAPF2, mLSAME, mRSAME, mLCOMB1, mRCOMB1, mLCOMB2, mRCOMB2; + s16 dLSAME, dRSAME, mLDIFF, mRDIFF, mLCOMB3, mRCOMB3, mLCOMB4, mRCOMB4; + s16 dLDIFF, dRDIFF, mLAPF1, mRAPF1, mLAPF2, mRAPF2, vLIN, vRIN; +} rev_preset_t; + +#define H(x) ((s16)(x)) +static const rev_preset_t rev_presets[10] = { + {0}, /* off */ + {0x26C0, 0x007D, 0x005B, 0x6D80, 0x54B8, H(0xBED0), 0x0000, 0x0000, H(0xBA80), /* room */ + 0x5800, 0x5300, 0x04D6, 0x0333, 0x03F0, 0x0227, 0x0374, 0x01EF, + 0x0334, 0x01B5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x01B4, 0x0136, 0x00B8, 0x005C, H(0x8000), H(0x8000)}, + {0x1F40, 0x0033, 0x0025, 0x70F0, 0x4FA8, H(0xBCE0), 0x4410, H(0xC0F0), H(0x9C00), /* studio A (small) */ + 0x5280, 0x4EC0, 0x03E4, 0x031B, 0x03A4, 0x02AF, 0x0372, 0x0266, + 0x031C, 0x025D, 0x025C, 0x018E, 0x022F, 0x0135, 0x01D2, 0x00B7, + 0x018F, 0x00B5, 0x00B4, 0x0080, 0x004C, 0x0026, H(0x8000), H(0x8000)}, + {0x4840, 0x00B1, 0x007F, 0x70F0, 0x4FA8, H(0xBCE0), 0x4510, H(0xBEF0), H(0xB4C0), /* studio B (medium) */ + 0x5280, 0x4EC0, 0x0904, 0x076B, 0x0824, 0x065F, 0x07A2, 0x0616, + 0x076C, 0x05ED, 0x05EC, 0x042E, 0x050F, 0x0305, 0x0462, 0x02B7, + 0x042F, 0x0265, 0x0264, 0x01B2, 0x0100, 0x0080, H(0x8000), H(0x8000)}, + {0x6FE0, 0x00E3, 0x00A9, 0x6F60, 0x4FA8, H(0xBCE0), 0x4510, H(0xBEF0), H(0xA680), /* studio C (large) */ + 0x5680, 0x52C0, 0x0DFB, 0x0B58, 0x0D09, 0x0A3C, 0x0BD9, 0x0973, + 0x0B59, 0x08DA, 0x08D9, 0x05E9, 0x07EC, 0x04B0, 0x06EF, 0x03D2, + 0x05EA, 0x031D, 0x031C, 0x0238, 0x0154, 0x00AA, H(0x8000), H(0x8000)}, + {0xADE0, 0x01A5, 0x0139, 0x6000, 0x5000, 0x4C00, H(0xB800), H(0xBC00), H(0xC000), /* hall */ + 0x6000, 0x5C00, 0x15BA, 0x11BB, 0x14C2, 0x10BD, 0x11BC, 0x0DC1, + 0x11C0, 0x0DC3, 0x0DC0, 0x09C1, 0x0BC4, 0x07C1, 0x0A00, 0x06CD, + 0x09C2, 0x05C1, 0x05C0, 0x041A, 0x0274, 0x013A, H(0x8000), H(0x8000)}, + {0xF6C0, 0x033D, 0x0231, 0x7E00, 0x5000, H(0xB400), H(0xB000), 0x4C00, H(0xB000), /* space */ + 0x6000, 0x5400, 0x1ED6, 0x1A31, 0x1D14, 0x183B, 0x1BC2, 0x16B2, + 0x1A32, 0x15EF, 0x15EE, 0x1055, 0x1334, 0x0F2D, 0x11F6, 0x0C5D, + 0x1056, 0x0AE1, 0x0AE0, 0x07A2, 0x0464, 0x0232, H(0x8000), H(0x8000)}, + {0x18040, 0x0001, 0x0001, 0x7FFF, 0x7FFF, 0x0000, 0x0000, 0x0000, H(0x8100), /* echo */ + 0x0000, 0x0000, 0x1FFF, 0x0FFF, 0x1005, 0x0005, 0x0000, 0x0000, + 0x1005, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x1004, 0x1002, 0x0004, 0x0002, H(0x8000), H(0x8000)}, + {0x18040, 0x0001, 0x0001, 0x7FFF, 0x7FFF, 0x0000, 0x0000, 0x0000, 0x0000, /* delay */ + 0x0000, 0x0000, 0x1FFF, 0x0FFF, 0x1005, 0x0005, 0x0000, 0x0000, + 0x1005, 0x0005, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x1004, 0x1002, 0x0004, 0x0002, H(0x8000), H(0x8000)}, + {0x3C00, 0x0017, 0x0013, 0x70F0, 0x4FA8, H(0xBCE0), 0x4510, H(0xBEF0), H(0x8500), /* pipe (half echo) */ + 0x5F80, 0x54C0, 0x0371, 0x02AF, 0x02E5, 0x01DF, 0x02B0, 0x01D7, + 0x0358, 0x026A, 0x01D6, 0x011E, 0x012D, 0x00B1, 0x011F, 0x0059, + 0x01A0, 0x00E3, 0x0058, 0x0040, 0x0028, 0x0014, H(0x8000), H(0x8000)}, +}; +#undef H + +static struct { + int mode; + rev_preset_t p; + s16 *buf; + u32 n, pos; /* work area size and position, in samples */ + s32 depth, depth_target, depth_step; + s32 inl, inr, half; /* input summed over two output samples */ + s32 outl, outr, prevl, prevr; /* last two outputs, for the upsampling */ +} rev; + +static void reverb_set(s32 mode, s32 depth, s32 delay, s32 feedback) +{ + if (mode < 0 || mode > 9) mode = 0; + rev.depth = rev.depth_target = clampi(depth, -0x8000, 0x7FFF); + rev.depth_step = 0; + if (mode != rev.mode) { + rev.mode = mode; + rev.p = rev_presets[mode]; + free(rev.buf); + rev.buf = 0; + rev.n = rev.pos = 0; + if (mode) { rev.n = rev.p.size / 2; rev.buf = calloc(rev.n, 2); if (!rev.buf) rev.mode = 0; } + rev.outl = rev.outr = rev.prevl = rev.prevr = 0; + } + if (mode == 7 || mode == 8) { /* echo, delay: delay and feedback set some registers */ + rev_preset_t *p = &rev.p; + s32 d16 = (clampi(delay, 0, 127) + 1) * 16; + *p = rev_presets[mode]; + p->mLSAME = (s16)(d16 * 2 - p->dAPF1); + p->mRSAME = (s16)(d16 - p->dAPF2); + p->mLCOMB1 = (s16)(p->mRCOMB1 + d16); + p->dLSAME = (s16)(p->dRSAME + d16); + p->mLAPF1 = (s16)(p->mLAPF2 + d16); + p->mRAPF1 = (s16)(d16 + (p->mLAPF2 >> 1)); + p->vWALL = (s16)(-(clampi(feedback, 0, 127) << 8)); + } +} + +static inline s16 *rv(s32 reg, s32 back) /* the sample at register offset reg (8-byte units) */ +{ + s32 a = (s32)rev.pos + reg * 4 - back; + a %= (s32)rev.n; + if (a < 0) a += (s32)rev.n; + return &rev.buf[a]; +} +static inline s16 sat16(s32 v) { return (s16)clampi(v, -32768, 32767); } +static inline s32 mul15(s32 a, s32 b) { return (a * b) >> 15; } + +/* one reverb step (at half the output rate) */ +static void reverb_step(s32 lin16, s32 rin16) +{ + const rev_preset_t *p = &rev.p; + s32 Lin = mul15(p->vLIN, lin16), Rin = mul15(p->vRIN, rin16); + s16 *t; + t = rv(p->mLSAME, 1); *rv(p->mLSAME, 0) = sat16(mul15(Lin + mul15(*rv(p->dLSAME, 0), p->vWALL) - *t, p->vIIR) + *t); + t = rv(p->mRSAME, 1); *rv(p->mRSAME, 0) = sat16(mul15(Rin + mul15(*rv(p->dRSAME, 0), p->vWALL) - *t, p->vIIR) + *t); + t = rv(p->mLDIFF, 1); *rv(p->mLDIFF, 0) = sat16(mul15(Lin + mul15(*rv(p->dRDIFF, 0), p->vWALL) - *t, p->vIIR) + *t); + t = rv(p->mRDIFF, 1); *rv(p->mRDIFF, 0) = sat16(mul15(Rin + mul15(*rv(p->dLDIFF, 0), p->vWALL) - *t, p->vIIR) + *t); + s32 L = mul15(p->vCOMB1, *rv(p->mLCOMB1, 0)) + mul15(p->vCOMB2, *rv(p->mLCOMB2, 0)) + + mul15(p->vCOMB3, *rv(p->mLCOMB3, 0)) + mul15(p->vCOMB4, *rv(p->mLCOMB4, 0)); + s32 R = mul15(p->vCOMB1, *rv(p->mRCOMB1, 0)) + mul15(p->vCOMB2, *rv(p->mRCOMB2, 0)) + + mul15(p->vCOMB3, *rv(p->mRCOMB3, 0)) + mul15(p->vCOMB4, *rv(p->mRCOMB4, 0)); + s32 x; + x = *rv(p->mLAPF1 - p->dAPF1, 0); L = sat16(L - mul15(p->vAPF1, x)); *rv(p->mLAPF1, 0) = (s16)L; L = mul15(L, p->vAPF1) + x; + x = *rv(p->mRAPF1 - p->dAPF1, 0); R = sat16(R - mul15(p->vAPF1, x)); *rv(p->mRAPF1, 0) = (s16)R; R = mul15(R, p->vAPF1) + x; + x = *rv(p->mLAPF2 - p->dAPF2, 0); L = sat16(L - mul15(p->vAPF2, x)); *rv(p->mLAPF2, 0) = (s16)L; L = mul15(L, p->vAPF2) + x; + x = *rv(p->mRAPF2 - p->dAPF2, 0); R = sat16(R - mul15(p->vAPF2, x)); *rv(p->mRAPF2, 0) = (s16)R; R = mul15(R, p->vAPF2) + x; + rev.prevl = rev.outl; rev.prevr = rev.outr; + rev.outl = mul15(sat16(L), rev.depth); + rev.outr = mul15(sat16(R), rev.depth); + if (++rev.pos >= rev.n) rev.pos = 0; +} + +/* feeds one output sample's wet input; adds the reverb's return to l, r */ +static void reverb_run(s32 wl, s32 wr, s32 *l, s32 *r) +{ + if (!rev.mode || !rev.buf) return; + rev.inl += wl; rev.inr += wr; + if (rev.half) { + reverb_step(sat16(rev.inl >> 1), sat16(rev.inr >> 1)); + rev.inl = rev.inr = 0; + *l += (rev.prevl + rev.outl) >> 1; /* between the last two outputs */ + *r += (rev.prevr + rev.outr) >> 1; + } else { + *l += rev.outl; + *r += rev.outr; + } + rev.half ^= 1; +} + +static void snd_mix(s16 *out, int frames) +{ + for (int f = 0; f < frames; f++) { + if (++htick >= 200) { + htick = 0; + tick_all(); + if (rev.depth_step) { /* snd_AutoReverb's fade */ + rev.depth += rev.depth_step; + if ((rev.depth_step > 0 && rev.depth >= rev.depth_target) || (rev.depth_step < 0 && rev.depth <= rev.depth_target)) { + rev.depth = rev.depth_target; + rev.depth_step = 0; + } + } + } + s32 l = 0, r = 0, wl = 0, wr = 0; + for (int i = 0; i < NVOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on) continue; + if (v->phase == PH_STOPPED) { + v->on = 0; + continue; + } + s32 vl = 0, vr = 0; + voice_run(v, &vl, &vr); + l += vl; + r += vr; + if (!v->stream && (v->tone.flags & 1)) { wl += vl; wr += vr; } + } + reverb_run(wl, wr, &l, &r); + out[f * 2] = (s16)clampi(l, -32768, 32767); + out[f * 2 + 1] = (s16)clampi(r, -32768, 32767); + } +} + +/* Called once per vsync (host/src/sdk/graph.c). The sound runs on the game's + * clock, as the PS2's SPU2 and EE share one: every vsync mixes exactly one + * field's worth of sound (960 frames at 50 Hz), so a VAG stream (a cut-scene's + * dialog and foley) and the sounds the game starts stay where the game is, + * whatever the speed of the machine. (Mixing as the output device consumed + * sound let the sound run on in real time while the game was held up: after + * every slow frame the mixer caught up by up to 200 ms at once, so the + * cut-scene streams ran ahead of the pictures.) + * The output keeps a small cushion: when it has (nearly) run dry (the start, + * a movie, a frame the machine was too slow for) silence is put in first; when + * it holds far too much (the output's clock is a little slower than the + * game's), a field's sound is dropped. */ +uint32_t uya_frame_us(void); +#define OUT_TARGET 3840 /* 80 ms of cushion */ +#define OUT_LOW 960 /* below: refill with silence */ +#define OUT_HIGH 14400 /* above (300 ms): drop */ +void uya_sound_pump(void) +{ + static s16 buf[1024 * 2], zero[1024 * 2]; + static u32 mixed; + static u64 due_us; /* game time not mixed yet (us * 48000) */ + if (snd_log > 0) { /* the music player's stream records (music.c, D_001517D0): their states */ + static s16 last[3] = {-1, -1, -1}; + static const char *name[3] = {"music", "dialog", "transition"}; + for (int i = 0; i < 3; i++) { + s16 st = *(volatile s16 *)(uintptr_t)(0x001517D0 + 0x34 + 0x1C * i + 0x0A); + if (st != last[i]) { uya_log("v%u 989snd: %s stream record state %d -> %d", uya_frame_count(), name[i], last[i], st); last[i] = st; } + } + } + due_us += (u64)uya_frame_us() * 48000u; + int n = (int)(due_us / 1000000u); + due_us -= (u64)n * 1000000u; + if (n > 1024) n = 1024; + snd_mix(buf, n); + int q = plat_stream_queued(); + if (q < OUT_LOW) + for (int k = OUT_TARGET - q; k > 0; k -= 1024) plat_stream_write(zero, k > 1024 ? 1024 : k); + if (q <= OUT_HIGH) plat_stream_write(buf, n); + u32 before = mixed; + mixed += (u32)n; + if (snd_log > 0 && mixed / (48000 * 5) != before / (48000 * 5)) { /* a status line every 5 s of sound */ + int nv = 0, nh = 0; + for (int i = 0; i < NVOICES; i++) nv += voices[i].on; + for (int i = 0; i < NHANDLERS; i++) nh += handlers[i].used && handlers[i].parent < 0; + uya_log("v%u 989snd: %u s of sound: %d voices, %d sounds playing, %d frames queued", uya_frame_count(), mixed / 48000, nv, nh, q); + for (int i = 0; i < NSTREAMS; i++) { + stream_t *s = &streams[i]; + if (!s->used) continue; + uya_log("989snd: stream %08X LSN %u at %u/%u%s, vol %d group %d", s->handle, s->lsn, + s->voice >= 0 ? voices[s->voice].nax : 0, s->size, s->paused ? " (paused)" : "", s->vol, s->group); + } + } +} + +/* ===================================================================== */ +/* the command protocol */ +/* ===================================================================== */ + +/* the EE's stream-safe CD state (snd_StreamSafeCdRead and friends, 989snd.c) */ +#define CDSAFE_COUNT 0x00137C00 +#define CDSAFE_ERROR 0x00137C10 + +static void init_once(void) +{ + if (snd_log >= 0) return; + snd_log = getenv("UYA_SNDLOG") != 0; + for (int i = 0; i < 32; i++) master_vol[i] = 0x400; + for (int i = 0; i < NSTREAMS; i++) streams[i].voice = -1; + for (int i = 0; i < NVOICES; i++) voices[i].handler = -1; +} + +/* testing: UYA_SNDTRACE=n logs the EE return addresses found on the + * translated code's stack (a heuristic backtrace) when sound n is played */ +static void snd_trace(int sfx) +{ + static int want = -2; + if (want == -2) { const char *e = getenv("UYA_SNDTRACE"); want = e ? atoi(e) : -1; } + if (want < 0 || sfx != want) return; + const char *rc_stack_scan(void); + uya_log("v%u sndtrace %d:%s", uya_frame_count(), sfx, rc_stack_scan()); +} + +static s32 play_sound(const s32 *a) +{ + snd_trace(a[1]); + bank_t *b = bank_get((u32)a[0]); + if (!b) { SLOG("989snd: play: no bank %08X", (u32)a[0]); return 0; } + u32 h = new_handle(); + int hi = handler_new(b, a[1], -1, -1, a[2], a[3], a[4], a[5], 0, -1, h); + SLOG("989snd: play bank %08X sound %d vol %d pan %d pm %d pb %d -> %08X", (u32)a[0], a[1], a[2], a[3], a[4], a[5], hi >= 0 ? h : 0); + if (hi < 0) return 0; + if (handlers[hi].done && !handler_has_voices(hi) && !handler_has_children(hi)) { handler_free(hi); return 0; } + return (s32)h; +} + +static int still_playing(u32 h) +{ + if (find_handler(h) >= 0) return 1; + return stream_active(find_stream(h)); +} + +s32 rac1_snd_command(s32 cmd, s32 size, const void *data) +{ + const s32 *a = (const s32 *)data; + init_once(); + if (snd_log && cmd != 0x09 && cmd != 0x19 && cmd != 0x32 && cmd != 0x4F && cmd != 0x21 && cmd != 0x15 && cmd != 0x11) { + char t[128] = ""; + int n = 0; + for (int i = 0; i < size / 4 && i < 8; i++) n += snprintf(t + n, sizeof t - n, " %X", (u32)a[i]); + uya_log("v%u 989snd command 0x%02X:%s", uya_frame_count(), cmd, t); + } + switch (cmd) { + case 0x06: { /* snd_UnloadBank */ + bank_t *b = bank_get((u32)a[0]); + if (!b) return 0; + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].bank == b) handler_free(i); + free(b->data); + memset(b, 0, sizeof *b); + return 0; + } + case 0x08: return 0; /* snd_ResolveBankXREFS */ + case 0x09: { /* snd_SetMasterVolume(group, vol) */ + s32 g = a[0], vol = clampi(a[1], 0, 0x400); + if (g < 0 || g >= 32 || g == 15) return 0; + master_vol[g] = vol; + for (int i = 0; i < NVOICES; i++) if (voices[i].on && voices[i].group == g && !voices[i].paused) voice_apply_volume(&voices[i]); + return 0; + } + case 0x0B: stereo_mode = a[0]; return 0; /* snd_SetPlaybackMode */ + case 0x0D: return 0; /* mixer mode */ + case 0x10: { /* snd_AutoReverb(core, depth, ticks, channels): fades the depth */ + s32 target = clampi(a[1], -0x8000, 0x7FFF), ticks = a[2] > 0 ? a[2] : 1; + rev.depth_target = target; + rev.depth_step = (target - rev.depth) / ticks; + if (rev.depth_step == 0) { rev.depth = target; } + return 0; + } + case 0x11: return play_sound(a); /* snd_PlaySoundVolPanPMPB */ + case 0x15: { /* snd_StopSound */ + int hi = find_handler((u32)a[0]); + if (hi >= 0 && !handlers[hi].done) SLOG("989snd: stop %08X (bank %08X sound %d)", (u32)a[0], handlers[hi].bank->handle, handlers[hi].sfx); + if (hi >= 0) handler_stop(hi); + stream_t *s = find_stream((u32)a[0]); + if (s) { s->next.on = 0; if (stream_active(s)) voice_kill(&voices[s->voice]); } + return 0; + } + case 0x16: case 0x17: { /* snd_PauseAllSoundsInGroup / Continue (group mask) */ + u32 mask = (u32)a[0]; + int pause = cmd == 0x16; + for (int i = 0; i < NHANDLERS; i++) + if (handlers[i].used && handlers[i].parent < 0 && (mask & (1u << (handlers[i].group & 31)))) handler_pause(i, pause); + for (int i = 0; i < NSTREAMS; i++) { + stream_t *s = &streams[i]; + if (!stream_active(s) || !(mask & (1u << (s->group & 31)))) continue; + voice_t *v = &voices[s->voice]; + if (pause) v->paused = 1; + else { v->paused = s->paused; stream_set_volume(s); } + } + return 0; + } + case 0x18: /* snd_StopAllSounds */ + for (int i = 0; i < NHANDLERS; i++) if (handlers[i].used && handlers[i].parent < 0) handler_stop(i); + return 0; + case 0x19: /* snd_SoundIsStillPlaying */ + return still_playing((u32)a[0]) ? a[0] : 0; + case 0x21: { /* snd_SetSoundParams(handle, mask, vol, pan, pm, pb) */ + u32 h = (u32)a[0]; + s32 mask = a[1]; + int hi = find_handler(h); + if (hi >= 0) { + if (mask & 3) handler_set_vol_pan(hi, (mask & 1) ? a[2] : VOLUME_DONT_CHANGE, (mask & 2) ? a[3] : PAN_DONT_CHANGE); + if (mask & 4) handler_set_pm(hi, a[4]); + if (mask & 8) handler_set_pb(hi, a[5]); + return (s32)h; + } + stream_t *s = find_stream(h); + if (stream_active(s)) { + if (mask & 1) s->vol = a[2]; + if (mask & 2) s->pan = a[3]; + stream_set_volume(s); + return (s32)h; + } + return 0; + } + case 0x2A: return 1; /* snd_InitVAGStreamingEx: streaming is on */ + case 0x2C: return (s32)stream_play(a); /* snd_PlayVAGStreamByLocEx */ + case 0x2D: case 0x2E: { /* snd_PauseVAGStream / snd_ContinueVAGStream */ + stream_t *s = find_stream((u32)a[0]); + if (!stream_active(s)) return 0; + s->paused = voices[s->voice].paused = cmd == 0x2D; + if (cmd == 0x2E) stream_set_volume(s); + return 0; + } + case 0x32: { /* snd_GetVAGStreamTimeRemaining (ms) */ + stream_t *s = find_stream((u32)a[0]); + if (!stream_active(s)) return 0; + voice_t *v = &voices[s->voice]; + u32 left = s->size > v->nax ? (s->size - v->nax) / 16 * 28 : 0; + return (s32)((u64)left * 1000 / s->rate); + } + case 0x34: /* snd_StopAllStreams */ + for (int i = 0; i < NSTREAMS; i++) if (streams[i].used) stream_free(&streams[i]); + return 0; + case 0x36: return 0; /* snd_StreamSafeCheckCDIdle */ + case 0x37: /* snd_StreamSafeCdBreak */ + EEW(CDSAFE_COUNT) = 0; + return 0; + case 0x38: { /* snd_StreamSafeCdRead(lsn, sectors, ee address) */ + int err = uya_disc_read((u32)a[0], (u32)a[1], EE(a[2])) != 0; + if (err) uya_log("989snd: stream-safe read of LSN %u (+%u) failed", (u32)a[0], (u32)a[1]); + EEW(CDSAFE_ERROR) = err ? 0x32 : 0; + EEW(CDSAFE_COUNT) = 0; + return 0; + } + case 0x4E: return 0; /* snd_SetGroupVoiceRange */ + case 0x4F: { /* snd_IsVAGStreamBuffered */ + stream_t *s = find_stream((u32)a[0]); + return stream_active(s) ? 1 : 0; + } + case 0x50: /* snd_SetReverbEx(core, mode, depth, delay, feedback) */ + reverb_set(a[1], a[2], a[3], a[4]); + return 0; + case 0x51: return 0; /* snd_PreAllocReverbWorkArea */ + default: + SLOG("989snd: command 0x%02X not handled", cmd); + return 0; + } +} + +/* snd_BankLoadByLoc (0x0012E060) */ +u32 rac1_snd_bank_load_lsn(s32 lsn, s32 off) +{ + u8 head[2048]; + init_once(); + if (uya_disc_read((u32)lsn, 1, head) != 0) return 0; + u32 size = bank_file_size(head + (off & 2047)); + if (size < 0x18 || size > (32u << 20)) { uya_log("989snd: no bank at LSN %d", lsn); return 0; } + u32 sectors = ((off & 2047) + size + 2047) / 2048; + u8 *b = malloc((size_t)sectors * 2048); + u32 h = 0; + if (b && uya_disc_read((u32)lsn + (u32)off / 2048, sectors, b) == 0) h = bank_add(b + (off & 2047), size); + free(b); + return h; +} + +/* snd_BankLoadFromEE (0x0012E1C8, command 0x57) */ +u32 rac1_snd_bank_load_ee(u32 addr) +{ + init_once(); + const u8 *src = EE(addr); + return bank_add(src, bank_file_size(src)); +} + +/* ---- replies owed to callbacks, delivered at the next flush ---- */ +typedef void (*snd_cb_t)(s32 result, s64 arg); +static struct { snd_cb_t fn; s64 arg; s32 result; } owed[256]; +static int nowed; + +void rac1_snd_owe(void *fn, s64 arg, s32 result) +{ + if (!fn) return; + if (nowed < 256) { owed[nowed].fn = (snd_cb_t)fn; owed[nowed].arg = arg; owed[nowed].result = result; nowed++; } + else uya_log("989snd: too many replies owed"); +} + +void rac1_snd_deliver(void) +{ + static struct { snd_cb_t fn; s64 arg; s32 result; } now[256]; + int n = nowed; + memcpy(now, owed, sizeof now[0] * (size_t)n); + nowed = 0; /* callbacks may send commands: those wait for the next flush */ + for (int i = 0; i < n; i++) now[i].fn(now[i].result, now[i].arg); +} + +/* snd_FlushSoundCommands (0x0012DDC0), host version: the IOP's replies are + * in, so the callbacks run; then, as the library does, a finished bank load + * from EE memory (command 0x57) gets its callback, and a finished + * stream-safe CD read gets the CD callback (snd_StreamSafeCdCallback's). + * Returns whether anything is still in flight: nothing is. */ +extern s32 func_0012EF48(s32); /* snd_StreamSafeCdSync */ +#define SDA(a) (*(volatile s32 *)(uintptr_t)(a)) +s32 rac1_snd_flush(void) +{ + static int depth; + if (depth > 4) return 0; + depth++; + rac1_snd_deliver(); + if (SDA(0x0015EDC8) != 0 && SDA(0x0015EE00) != -1) { /* bank load from EE done */ + snd_cb_t fn = (snd_cb_t)(uintptr_t)(u32)SDA(0x0015EDD0); + s64 arg = *(volatile s64 *)(uintptr_t)0x0015EDD8; + s32 r = SDA(0x0015EE00); + SDA(0x0015EDD0) = 0; + *(volatile s64 *)(uintptr_t)0x0015EDD8 = 0; + SDA(0x0015EE00) = 0; + SDA(0x0015EDC8) = 0; + if (fn) fn(r, arg); + } + if (SDA(0x0015ED94) != 0) { /* stream-safe CD read requested */ + func_0012EF48(1); + if (SDA(0x0015ED98) != 0) { + void (*cb)(s32) = (void (*)(s32))(uintptr_t)(u32)SDA(0x0015ED90); + SDA(0x0015ED94) = 0; + SDA(0x0015ED98) = 0; + if (cb) cb(1); + } + } + depth--; + return 0; +} diff --git a/games/rac1/host/src/rac1/snd989_tables.h b/games/rac1/host/src/rac1/snd989_tables.h new file mode 100644 index 0000000..c273946 --- /dev/null +++ b/games/rac1/host/src/rac1/snd989_tables.h @@ -0,0 +1,307 @@ +/* Tables for the 989snd host driver (host/src/rac1/snd989.c): the SPU2's note-to-pitch table, + * 989snd's pan table and the SPU's 4-tap Gaussian interpolation table. Values from the + * jak-project 989snd reimplementation (ISC licence, see THIRD_PARTY_NOTICES.md). */ +static const unsigned short snd_note_pitch[140] = { + 0x8000, 0x879C, 0x8FAC, 0x9837, 0xA145, 0xAADC, 0xB504, 0xBFC8, 0xCB2F, 0xD744, 0xE411, 0xF1A1, + 0x8000, 0x800E, 0x801D, 0x802C, 0x803B, 0x804A, 0x8058, 0x8067, 0x8076, 0x8085, 0x8094, 0x80A3, + 0x80B1, 0x80C0, 0x80CF, 0x80DE, 0x80ED, 0x80FC, 0x810B, 0x811A, 0x8129, 0x8138, 0x8146, 0x8155, + 0x8164, 0x8173, 0x8182, 0x8191, 0x81A0, 0x81AF, 0x81BE, 0x81CD, 0x81DC, 0x81EB, 0x81FA, 0x8209, + 0x8218, 0x8227, 0x8236, 0x8245, 0x8254, 0x8263, 0x8272, 0x8282, 0x8291, 0x82A0, 0x82AF, 0x82BE, + 0x82CD, 0x82DC, 0x82EB, 0x82FA, 0x830A, 0x8319, 0x8328, 0x8337, 0x8346, 0x8355, 0x8364, 0x8374, + 0x8383, 0x8392, 0x83A1, 0x83B0, 0x83C0, 0x83CF, 0x83DE, 0x83ED, 0x83FD, 0x840C, 0x841B, 0x842A, + 0x843A, 0x8449, 0x8458, 0x8468, 0x8477, 0x8486, 0x8495, 0x84A5, 0x84B4, 0x84C3, 0x84D3, 0x84E2, + 0x84F1, 0x8501, 0x8510, 0x8520, 0x852F, 0x853E, 0x854E, 0x855D, 0x856D, 0x857C, 0x858B, 0x859B, + 0x85AA, 0x85BA, 0x85C9, 0x85D9, 0x85E8, 0x85F8, 0x8607, 0x8617, 0x8626, 0x8636, 0x8645, 0x8655, + 0x8664, 0x8674, 0x8683, 0x8693, 0x86A2, 0x86B2, 0x86C1, 0x86D1, 0x86E0, 0x86F0, 0x8700, 0x870F, + 0x871F, 0x872E, 0x873E, 0x874E, 0x875D, 0x876D, 0x877D, 0x878C, +}; +static const short snd_pan_table[181][2] = { + {0x3fff, 0x0000}, {0x3ffe, 0x008e}, {0x3ffc, 0x011d}, {0x3ff9, 0x01ac}, {0x3ff5, 0x023b}, {0x3fef, 0x02ca}, + {0x3fe8, 0x0359}, {0x3fe0, 0x03e8}, {0x3fd7, 0x0476}, {0x3fcc, 0x0505}, {0x3fc0, 0x0593}, {0x3fb3, 0x0622}, + {0x3fa5, 0x06b0}, {0x3f95, 0x073e}, {0x3f84, 0x07cc}, {0x3f72, 0x085a}, {0x3f5f, 0x08e8}, {0x3f4b, 0x0975}, + {0x3f35, 0x0a02}, {0x3f1e, 0x0a8f}, {0x3f06, 0x0b1c}, {0x3eec, 0x0ba9}, {0x3ed1, 0x0c36}, {0x3eb6, 0x0cc2}, + {0x3e98, 0x0d4e}, {0x3e7a, 0x0dd9}, {0x3e5b, 0x0e65}, {0x3e3a, 0x0ef0}, {0x3e18, 0x0f7b}, {0x3df5, 0x1005}, + {0x3dd0, 0x1090}, {0x3dab, 0x111a}, {0x3d84, 0x11a3}, {0x3d5c, 0x122d}, {0x3d33, 0x12b5}, {0x3d08, 0x133e}, + {0x3cdd, 0x13c6}, {0x3cb0, 0x144e}, {0x3c82, 0x14d5}, {0x3c53, 0x155c}, {0x3c22, 0x15e3}, {0x3bf1, 0x1669}, + {0x3bbe, 0x16ef}, {0x3b8b, 0x1774}, {0x3b56, 0x17f9}, {0x3b1f, 0x187d}, {0x3ae8, 0x1901}, {0x3ab0, 0x1984}, + {0x3a76, 0x1a07}, {0x3a3b, 0x1a89}, {0x3a00, 0x1b0b}, {0x39c3, 0x1b8d}, {0x3984, 0x1c0d}, {0x3945, 0x1c8e}, + {0x3905, 0x1d0d}, {0x38c3, 0x1d8c}, {0x3881, 0x1e0b}, {0x383d, 0x1e89}, {0x37f8, 0x1f06}, {0x37b3, 0x1f83}, + {0x376c, 0x1fff}, {0x3724, 0x207b}, {0x36da, 0x20f5}, {0x3690, 0x2170}, {0x3645, 0x21e9}, {0x35f9, 0x2262}, + {0x35ab, 0x22da}, {0x355d, 0x2352}, {0x350e, 0x23c9}, {0x34bd, 0x243f}, {0x346c, 0x24b4}, {0x3419, 0x2529}, + {0x33c6, 0x259d}, {0x3371, 0x2610}, {0x331c, 0x2683}, {0x32c5, 0x26f5}, {0x326d, 0x2766}, {0x3215, 0x27d6}, + {0x31bb, 0x2846}, {0x3161, 0x28b4}, {0x3106, 0x2922}, {0x30a9, 0x298f}, {0x304c, 0x29fc}, {0x2fee, 0x2a67}, + {0x2f8e, 0x2ad2}, {0x2f2e, 0x2b3c}, {0x2ecd, 0x2ba5}, {0x2e6b, 0x2c0d}, {0x2e08, 0x2c74}, {0x2da5, 0x2cda}, + {0x2d40, 0x2d40}, {0x2cda, 0x2da5}, {0x2c74, 0x2e08}, {0x2c0d, 0x2e6b}, {0x2ba5, 0x2ecd}, {0x2b3c, 0x2f2e}, + {0x2ad2, 0x2f8e}, {0x2a67, 0x2fee}, {0x29fc, 0x304c}, {0x298f, 0x30a9}, {0x2922, 0x3106}, {0x28b4, 0x3161}, + {0x2846, 0x31bb}, {0x27d6, 0x3215}, {0x2766, 0x326d}, {0x26f5, 0x32c5}, {0x2683, 0x331c}, {0x2610, 0x3371}, + {0x259d, 0x33c6}, {0x2529, 0x3419}, {0x24b4, 0x346c}, {0x243f, 0x34bd}, {0x23c9, 0x350e}, {0x2352, 0x355d}, + {0x22da, 0x35ab}, {0x2262, 0x35f9}, {0x21e9, 0x3645}, {0x2170, 0x3690}, {0x20f5, 0x36da}, {0x207b, 0x3724}, + {0x1fff, 0x376c}, {0x1f83, 0x37b3}, {0x1f06, 0x37f8}, {0x1e89, 0x383d}, {0x1e0b, 0x3881}, {0x1d8c, 0x38c3}, + {0x1d0d, 0x3905}, {0x1c8e, 0x3945}, {0x1c0d, 0x3984}, {0x1b8d, 0x39c3}, {0x1b0b, 0x3a00}, {0x1a89, 0x3a3b}, + {0x1a07, 0x3a76}, {0x1984, 0x3ab0}, {0x1901, 0x3ae8}, {0x187d, 0x3b1f}, {0x17f9, 0x3b56}, {0x1774, 0x3b8b}, + {0x16ef, 0x3bbe}, {0x1669, 0x3bf1}, {0x15e3, 0x3c22}, {0x155c, 0x3c53}, {0x14d5, 0x3c82}, {0x144e, 0x3cb0}, + {0x13c6, 0x3cdd}, {0x133e, 0x3d08}, {0x12b5, 0x3d33}, {0x122d, 0x3d5c}, {0x11a3, 0x3d84}, {0x111a, 0x3dab}, + {0x1090, 0x3dd0}, {0x1005, 0x3df5}, {0x0f7b, 0x3e18}, {0x0ef0, 0x3e3a}, {0x0e65, 0x3e5b}, {0x0dd9, 0x3e7a}, + {0x0d4e, 0x3e98}, {0x0cc2, 0x3eb6}, {0x0c36, 0x3ed1}, {0x0ba9, 0x3eec}, {0x0b1c, 0x3f06}, {0x0a8f, 0x3f1e}, + {0x0a02, 0x3f35}, {0x0975, 0x3f4b}, {0x08e8, 0x3f5f}, {0x085a, 0x3f72}, {0x07cc, 0x3f84}, {0x073e, 0x3f95}, + {0x06b0, 0x3fa5}, {0x0622, 0x3fb3}, {0x0593, 0x3fc0}, {0x0505, 0x3fcc}, {0x0476, 0x3fd7}, {0x03e8, 0x3fe0}, + {0x0359, 0x3fe8}, {0x02ca, 0x3fef}, {0x023b, 0x3ff5}, {0x01ac, 0x3ff9}, {0x011d, 0x3ffc}, {0x008e, 0x3ffe}, +}; +static const short snd_interp[256][4] = { + {0x12C7, 0x59B3, 0x1307, -0x0001}, + {0x1288, 0x59B2, 0x1347, -0x0001}, + {0x1249, 0x59B0, 0x1388, -0x0001}, + {0x120B, 0x59AD, 0x13C9, -0x0001}, + {0x11CD, 0x59A9, 0x140B, -0x0001}, + {0x118F, 0x59A4, 0x144D, -0x0001}, + {0x1153, 0x599E, 0x1490, -0x0001}, + {0x1116, 0x5997, 0x14D4, -0x0001}, + {0x10DB, 0x598F, 0x1517, -0x0001}, + {0x109F, 0x5986, 0x155C, -0x0001}, + {0x1065, 0x597C, 0x15A0, -0x0001}, + {0x102A, 0x5971, 0x15E6, -0x0001}, + {0x0FF1, 0x5965, 0x162C, -0x0001}, + {0x0FB7, 0x5958, 0x1672, -0x0001}, + {0x0F7F, 0x5949, 0x16B9, -0x0001}, + {0x0F46, 0x593A, 0x1700, -0x0001}, + {0x0F0F, 0x592A, 0x1747, 0x0000}, + {0x0ED7, 0x5919, 0x1790, 0x0000}, + {0x0EA1, 0x5907, 0x17D8, 0x0000}, + {0x0E6B, 0x58F4, 0x1821, 0x0000}, + {0x0E35, 0x58E0, 0x186B, 0x0000}, + {0x0E00, 0x58CB, 0x18B5, 0x0000}, + {0x0DCB, 0x58B5, 0x1900, 0x0000}, + {0x0D97, 0x589E, 0x194B, 0x0001}, + {0x0D63, 0x5886, 0x1996, 0x0001}, + {0x0D30, 0x586D, 0x19E2, 0x0001}, + {0x0CFD, 0x5853, 0x1A2E, 0x0001}, + {0x0CCB, 0x5838, 0x1A7B, 0x0002}, + {0x0C99, 0x581C, 0x1AC8, 0x0002}, + {0x0C68, 0x57FF, 0x1B16, 0x0002}, + {0x0C38, 0x57E2, 0x1B64, 0x0003}, + {0x0C07, 0x57C3, 0x1BB3, 0x0003}, + {0x0BD8, 0x57A3, 0x1C02, 0x0003}, + {0x0BA9, 0x5782, 0x1C51, 0x0004}, + {0x0B7A, 0x5761, 0x1CA1, 0x0004}, + {0x0B4C, 0x573E, 0x1CF1, 0x0005}, + {0x0B1E, 0x571B, 0x1D42, 0x0005}, + {0x0AF1, 0x56F6, 0x1D93, 0x0006}, + {0x0AC4, 0x56D1, 0x1DE5, 0x0007}, + {0x0A98, 0x56AB, 0x1E37, 0x0007}, + {0x0A6C, 0x5684, 0x1E89, 0x0008}, + {0x0A40, 0x565B, 0x1EDC, 0x0009}, + {0x0A16, 0x5632, 0x1F2F, 0x0009}, + {0x09EB, 0x5609, 0x1F82, 0x000A}, + {0x09C1, 0x55DE, 0x1FD6, 0x000B}, + {0x0998, 0x55B2, 0x202A, 0x000C}, + {0x096F, 0x5585, 0x207F, 0x000D}, + {0x0946, 0x5558, 0x20D4, 0x000E}, + {0x091E, 0x5529, 0x2129, 0x000F}, + {0x08F7, 0x54FA, 0x217F, 0x0010}, + {0x08D0, 0x54CA, 0x21D5, 0x0011}, + {0x08A9, 0x5499, 0x222C, 0x0012}, + {0x0883, 0x5467, 0x2282, 0x0013}, + {0x085D, 0x5434, 0x22DA, 0x0015}, + {0x0838, 0x5401, 0x2331, 0x0016}, + {0x0813, 0x53CC, 0x2389, 0x0018}, + {0x07EF, 0x5397, 0x23E1, 0x0019}, + {0x07CB, 0x5361, 0x2439, 0x001B}, + {0x07A7, 0x532A, 0x2492, 0x001C}, + {0x0784, 0x52F3, 0x24EB, 0x001E}, + {0x0762, 0x52BA, 0x2545, 0x0020}, + {0x0740, 0x5281, 0x259E, 0x0021}, + {0x071E, 0x5247, 0x25F8, 0x0023}, + {0x06FD, 0x520C, 0x2653, 0x0025}, + {0x06DC, 0x51D0, 0x26AD, 0x0027}, + {0x06BB, 0x5194, 0x2708, 0x0029}, + {0x069B, 0x5156, 0x2763, 0x002C}, + {0x067C, 0x5118, 0x27BE, 0x002E}, + {0x065C, 0x50DA, 0x281A, 0x0030}, + {0x063E, 0x509A, 0x2876, 0x0033}, + {0x061F, 0x505A, 0x28D2, 0x0035}, + {0x0601, 0x5019, 0x292E, 0x0038}, + {0x05E4, 0x4FD7, 0x298B, 0x003A}, + {0x05C7, 0x4F95, 0x29E7, 0x003D}, + {0x05AA, 0x4F52, 0x2A44, 0x0040}, + {0x058E, 0x4F0E, 0x2AA1, 0x0043}, + {0x0572, 0x4EC9, 0x2AFF, 0x0046}, + {0x0556, 0x4E84, 0x2B5C, 0x0049}, + {0x053B, 0x4E3E, 0x2BBA, 0x004D}, + {0x0520, 0x4DF7, 0x2C18, 0x0050}, + {0x0506, 0x4DB0, 0x2C76, 0x0054}, + {0x04EC, 0x4D68, 0x2CD4, 0x0057}, + {0x04D2, 0x4D20, 0x2D33, 0x005B}, + {0x04B9, 0x4CD7, 0x2D91, 0x005F}, + {0x04A0, 0x4C8D, 0x2DF0, 0x0063}, + {0x0488, 0x4C42, 0x2E4F, 0x0067}, + {0x0470, 0x4BF7, 0x2EAE, 0x006B}, + {0x0458, 0x4BAC, 0x2F0D, 0x006F}, + {0x0441, 0x4B5F, 0x2F6C, 0x0074}, + {0x042A, 0x4B13, 0x2FCC, 0x0078}, + {0x0413, 0x4AC5, 0x302B, 0x007D}, + {0x03FC, 0x4A77, 0x308B, 0x0082}, + {0x03E7, 0x4A29, 0x30EA, 0x0087}, + {0x03D1, 0x49D9, 0x314A, 0x008C}, + {0x03BC, 0x498A, 0x31AA, 0x0091}, + {0x03A7, 0x493A, 0x3209, 0x0096}, + {0x0392, 0x48E9, 0x3269, 0x009C}, + {0x037E, 0x4898, 0x32C9, 0x00A1}, + {0x036A, 0x4846, 0x3329, 0x00A7}, + {0x0356, 0x47F4, 0x3389, 0x00AD}, + {0x0343, 0x47A1, 0x33E9, 0x00B3}, + {0x0330, 0x474E, 0x3449, 0x00BA}, + {0x031D, 0x46FA, 0x34A9, 0x00C0}, + {0x030B, 0x46A6, 0x3509, 0x00C7}, + {0x02F9, 0x4651, 0x3569, 0x00CD}, + {0x02E7, 0x45FC, 0x35C9, 0x00D4}, + {0x02D6, 0x45A6, 0x3629, 0x00DB}, + {0x02C4, 0x4550, 0x3689, 0x00E3}, + {0x02B4, 0x44FA, 0x36E8, 0x00EA}, + {0x02A3, 0x44A3, 0x3748, 0x00F2}, + {0x0293, 0x444C, 0x37A8, 0x00FA}, + {0x0283, 0x43F4, 0x3807, 0x0101}, + {0x0273, 0x439C, 0x3867, 0x010A}, + {0x0264, 0x4344, 0x38C6, 0x0112}, + {0x0255, 0x42EB, 0x3926, 0x011B}, + {0x0246, 0x4292, 0x3985, 0x0123}, + {0x0237, 0x4239, 0x39E4, 0x012C}, + {0x0229, 0x41DF, 0x3A43, 0x0135}, + {0x021B, 0x4185, 0x3AA2, 0x013F}, + {0x020D, 0x412A, 0x3B00, 0x0148}, + {0x0200, 0x40D0, 0x3B5F, 0x0152}, + {0x01F2, 0x4074, 0x3BBD, 0x015C}, + {0x01E5, 0x4019, 0x3C1B, 0x0166}, + {0x01D9, 0x3FBD, 0x3C79, 0x0171}, + {0x01CC, 0x3F62, 0x3CD7, 0x017B}, + {0x01C0, 0x3F05, 0x3D35, 0x0186}, + {0x01B4, 0x3EA9, 0x3D92, 0x0191}, + {0x01A8, 0x3E4C, 0x3DEF, 0x019C}, + {0x019C, 0x3DEF, 0x3E4C, 0x01A8}, + {0x0191, 0x3D92, 0x3EA9, 0x01B4}, + {0x0186, 0x3D35, 0x3F05, 0x01C0}, + {0x017B, 0x3CD7, 0x3F62, 0x01CC}, + {0x0171, 0x3C79, 0x3FBD, 0x01D9}, + {0x0166, 0x3C1B, 0x4019, 0x01E5}, + {0x015C, 0x3BBD, 0x4074, 0x01F2}, + {0x0152, 0x3B5F, 0x40D0, 0x0200}, + {0x0148, 0x3B00, 0x412A, 0x020D}, + {0x013F, 0x3AA2, 0x4185, 0x021B}, + {0x0135, 0x3A43, 0x41DF, 0x0229}, + {0x012C, 0x39E4, 0x4239, 0x0237}, + {0x0123, 0x3985, 0x4292, 0x0246}, + {0x011B, 0x3926, 0x42EB, 0x0255}, + {0x0112, 0x38C6, 0x4344, 0x0264}, + {0x010A, 0x3867, 0x439C, 0x0273}, + {0x0101, 0x3807, 0x43F4, 0x0283}, + {0x00FA, 0x37A8, 0x444C, 0x0293}, + {0x00F2, 0x3748, 0x44A3, 0x02A3}, + {0x00EA, 0x36E8, 0x44FA, 0x02B4}, + {0x00E3, 0x3689, 0x4550, 0x02C4}, + {0x00DB, 0x3629, 0x45A6, 0x02D6}, + {0x00D4, 0x35C9, 0x45FC, 0x02E7}, + {0x00CD, 0x3569, 0x4651, 0x02F9}, + {0x00C7, 0x3509, 0x46A6, 0x030B}, + {0x00C0, 0x34A9, 0x46FA, 0x031D}, + {0x00BA, 0x3449, 0x474E, 0x0330}, + {0x00B3, 0x33E9, 0x47A1, 0x0343}, + {0x00AD, 0x3389, 0x47F4, 0x0356}, + {0x00A7, 0x3329, 0x4846, 0x036A}, + {0x00A1, 0x32C9, 0x4898, 0x037E}, + {0x009C, 0x3269, 0x48E9, 0x0392}, + {0x0096, 0x3209, 0x493A, 0x03A7}, + {0x0091, 0x31AA, 0x498A, 0x03BC}, + {0x008C, 0x314A, 0x49D9, 0x03D1}, + {0x0087, 0x30EA, 0x4A29, 0x03E7}, + {0x0082, 0x308B, 0x4A77, 0x03FC}, + {0x007D, 0x302B, 0x4AC5, 0x0413}, + {0x0078, 0x2FCC, 0x4B13, 0x042A}, + {0x0074, 0x2F6C, 0x4B5F, 0x0441}, + {0x006F, 0x2F0D, 0x4BAC, 0x0458}, + {0x006B, 0x2EAE, 0x4BF7, 0x0470}, + {0x0067, 0x2E4F, 0x4C42, 0x0488}, + {0x0063, 0x2DF0, 0x4C8D, 0x04A0}, + {0x005F, 0x2D91, 0x4CD7, 0x04B9}, + {0x005B, 0x2D33, 0x4D20, 0x04D2}, + {0x0057, 0x2CD4, 0x4D68, 0x04EC}, + {0x0054, 0x2C76, 0x4DB0, 0x0506}, + {0x0050, 0x2C18, 0x4DF7, 0x0520}, + {0x004D, 0x2BBA, 0x4E3E, 0x053B}, + {0x0049, 0x2B5C, 0x4E84, 0x0556}, + {0x0046, 0x2AFF, 0x4EC9, 0x0572}, + {0x0043, 0x2AA1, 0x4F0E, 0x058E}, + {0x0040, 0x2A44, 0x4F52, 0x05AA}, + {0x003D, 0x29E7, 0x4F95, 0x05C7}, + {0x003A, 0x298B, 0x4FD7, 0x05E4}, + {0x0038, 0x292E, 0x5019, 0x0601}, + {0x0035, 0x28D2, 0x505A, 0x061F}, + {0x0033, 0x2876, 0x509A, 0x063E}, + {0x0030, 0x281A, 0x50DA, 0x065C}, + {0x002E, 0x27BE, 0x5118, 0x067C}, + {0x002C, 0x2763, 0x5156, 0x069B}, + {0x0029, 0x2708, 0x5194, 0x06BB}, + {0x0027, 0x26AD, 0x51D0, 0x06DC}, + {0x0025, 0x2653, 0x520C, 0x06FD}, + {0x0023, 0x25F8, 0x5247, 0x071E}, + {0x0021, 0x259E, 0x5281, 0x0740}, + {0x0020, 0x2545, 0x52BA, 0x0762}, + {0x001E, 0x24EB, 0x52F3, 0x0784}, + {0x001C, 0x2492, 0x532A, 0x07A7}, + {0x001B, 0x2439, 0x5361, 0x07CB}, + {0x0019, 0x23E1, 0x5397, 0x07EF}, + {0x0018, 0x2389, 0x53CC, 0x0813}, + {0x0016, 0x2331, 0x5401, 0x0838}, + {0x0015, 0x22DA, 0x5434, 0x085D}, + {0x0013, 0x2282, 0x5467, 0x0883}, + {0x0012, 0x222C, 0x5499, 0x08A9}, + {0x0011, 0x21D5, 0x54CA, 0x08D0}, + {0x0010, 0x217F, 0x54FA, 0x08F7}, + {0x000F, 0x2129, 0x5529, 0x091E}, + {0x000E, 0x20D4, 0x5558, 0x0946}, + {0x000D, 0x207F, 0x5585, 0x096F}, + {0x000C, 0x202A, 0x55B2, 0x0998}, + {0x000B, 0x1FD6, 0x55DE, 0x09C1}, + {0x000A, 0x1F82, 0x5609, 0x09EB}, + {0x0009, 0x1F2F, 0x5632, 0x0A16}, + {0x0009, 0x1EDC, 0x565B, 0x0A40}, + {0x0008, 0x1E89, 0x5684, 0x0A6C}, + {0x0007, 0x1E37, 0x56AB, 0x0A98}, + {0x0007, 0x1DE5, 0x56D1, 0x0AC4}, + {0x0006, 0x1D93, 0x56F6, 0x0AF1}, + {0x0005, 0x1D42, 0x571B, 0x0B1E}, + {0x0005, 0x1CF1, 0x573E, 0x0B4C}, + {0x0004, 0x1CA1, 0x5761, 0x0B7A}, + {0x0004, 0x1C51, 0x5782, 0x0BA9}, + {0x0003, 0x1C02, 0x57A3, 0x0BD8}, + {0x0003, 0x1BB3, 0x57C3, 0x0C07}, + {0x0003, 0x1B64, 0x57E2, 0x0C38}, + {0x0002, 0x1B16, 0x57FF, 0x0C68}, + {0x0002, 0x1AC8, 0x581C, 0x0C99}, + {0x0002, 0x1A7B, 0x5838, 0x0CCB}, + {0x0001, 0x1A2E, 0x5853, 0x0CFD}, + {0x0001, 0x19E2, 0x586D, 0x0D30}, + {0x0001, 0x1996, 0x5886, 0x0D63}, + {0x0001, 0x194B, 0x589E, 0x0D97}, + {0x0000, 0x1900, 0x58B5, 0x0DCB}, + {0x0000, 0x18B5, 0x58CB, 0x0E00}, + {0x0000, 0x186B, 0x58E0, 0x0E35}, + {0x0000, 0x1821, 0x58F4, 0x0E6B}, + {0x0000, 0x17D8, 0x5907, 0x0EA1}, + {0x0000, 0x1790, 0x5919, 0x0ED7}, + {0x0000, 0x1747, 0x592A, 0x0F0F}, + {-0x0001, 0x1700, 0x593A, 0x0F46}, + {-0x0001, 0x16B9, 0x5949, 0x0F7F}, + {-0x0001, 0x1672, 0x5958, 0x0FB7}, + {-0x0001, 0x162C, 0x5965, 0x0FF1}, + {-0x0001, 0x15E6, 0x5971, 0x102A}, + {-0x0001, 0x15A0, 0x597C, 0x1065}, + {-0x0001, 0x155C, 0x5986, 0x109F}, + {-0x0001, 0x1517, 0x598F, 0x10DB}, + {-0x0001, 0x14D4, 0x5997, 0x1116}, + {-0x0001, 0x1490, 0x599E, 0x1153}, + {-0x0001, 0x144D, 0x59A4, 0x118F}, + {-0x0001, 0x140B, 0x59A9, 0x11CD}, + {-0x0001, 0x13C9, 0x59AD, 0x120B}, + {-0x0001, 0x1388, 0x59B0, 0x1249}, + {-0x0001, 0x1347, 0x59B2, 0x1288}, + {-0x0001, 0x1307, 0x59B3, 0x12C7}, +}; diff --git a/games/rac1/host/src/runtime.c b/games/rac1/host/src/runtime.c new file mode 100644 index 0000000..6a00ca5 --- /dev/null +++ b/games/rac1/host/src/runtime.c @@ -0,0 +1,164 @@ +/* Host runtime: loads data sections from the user's own ELFs into PS2 RAM, + * writes x86 jumps at the PS2 address of every host-implemented function, + * and keeps the log of unimplemented calls. */ +#include "uya_runtime.h" +#include +#include +#include +#include + +/* ---- ELF32 little-endian section loader ---- */ +typedef struct { uint8_t ident[16]; uint16_t type, machine; uint32_t version, entry, phoff, shoff, flags; + uint16_t ehsize, phentsize, phnum, shentsize, shnum, shstrndx; } Ehdr; +typedef struct { uint32_t name, type, flags, addr, offset, size, link, info, addralign, entsize; } Shdr; + +/* A file in the disc image's root directory (ISO 9660), read whole: the + * static recompilation runs off the disc, with nothing else from the game. */ +int uya_disc_read(uint32_t lsn, uint32_t count, void *buf); +uint8_t *uya_disc_file(const char *name, uint32_t *size) +{ + uint8_t pvd[2048]; + if (uya_disc_read(16, 1, pvd) || memcmp(pvd + 1, "CD001", 5)) return 0; + uint32_t dlsn = *(uint32_t *)(pvd + 156 + 2), dlen = *(uint32_t *)(pvd + 156 + 10); + uint32_t nsec = (dlen + 2047) / 2048; + uint8_t *dir = malloc(nsec * 2048); + if (!dir || uya_disc_read(dlsn, nsec, dir)) { free(dir); return 0; } + size_t nl = strlen(name); + for (uint32_t o = 0; o < nsec * 2048;) { + uint8_t len = dir[o]; + if (!len) { o = (o / 2048 + 1) * 2048; continue; } + uint8_t fl = dir[o + 32]; + const char *fn = (const char *)dir + o + 33; + if (fl >= nl && !memcmp(fn, name, nl) && (fl == nl || fn[nl] == ';')) { + uint32_t flsn = *(uint32_t *)(dir + o + 2), fsz = *(uint32_t *)(dir + o + 10); + free(dir); + uint8_t *d = malloc(((fsz + 2047) / 2048) * 2048); + if (!d || uya_disc_read(flsn, (fsz + 2047) / 2048, d)) { free(d); return 0; } + *size = fsz; + return d; + } + o += len; + } + free(dir); + return 0; +} + +int uya_load_elf_sections(const char *path, const char *const *sections) +{ + uint8_t *d; + long n; + if (!strncmp(path, "disc:", 5)) { /* from the disc image */ + uint32_t sz; + d = uya_disc_file(path + 5, &sz); + if (!d) { uya_log("%s is not on the disc image", path + 5); return -1; } + n = (long)sz; + } else { + FILE *f = fopen(path, "rb"); + if (!f) { uya_log("cannot open %s", path); return -1; } + fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET); + d = malloc(n); + if (!d || fread(d, 1, n, f) != (size_t)n) { fclose(f); return -1; } + fclose(f); + } + Ehdr *e = (Ehdr *)d; + if (memcmp(e->ident, "\x7f" "ELF", 4) || e->ident[4] != 1 || e->machine != 8) { + uya_log("%s is not a 32-bit MIPS ELF", path); free(d); return -1; + } + Shdr *sh = (Shdr *)(d + e->shoff); + const char *strtab = (const char *)d + sh[e->shstrndx].offset; + int loaded = 0; + for (int i = 0; i < e->shnum; i++) { + const char *nm = strtab + sh[i].name; + for (const char *const *s = sections; *s; s++) { + if (strcmp(nm, *s)) continue; + uint32_t a = sh[i].addr, sz = sh[i].size; + if (a < PS2_RAM_LO || a + sz > PS2_RAM_HI) { uya_log("%s %s out of RAM", path, nm); continue; } + if (sh[i].type == 8 /* NOBITS */) memset((void *)(uintptr_t)a, 0, sz); + else memcpy((void *)(uintptr_t)a, d + sh[i].offset, sz); + uya_log("loaded %-12s 0x%08X-0x%08X from %s", nm, a, a + sz, path); + loaded++; + } + } + free(d); + return loaded; +} + +/* ---- trampolines ---- */ +void uya_fill_code_traps(uint32_t lo, uint32_t hi) +{ + memset((void *)(uintptr_t)lo, 0xCC, hi - lo); /* int3: unknown PS2 code */ +} + +void uya_install_tramp_table(const uya_tramp_t *table, int count) +{ + for (int i = 0; i < count; i++) { + uint8_t *p = (uint8_t *)(uintptr_t)table[i].ps2_addr; + int32_t rel = (int32_t)((uintptr_t)table[i].host_fn - ((uintptr_t)p + 5)); + p[0] = 0xE9; /* jmp rel32 */ + memcpy(p + 1, &rel, 4); + } + plat_flush_icache((void *)(uintptr_t)PS2_RAM_LO, PS2_RAM_HI - PS2_RAM_LO); + uya_log("installed %d trampolines", count); +} + +void uya_install_trampolines(void) { uya_install_tramp_table(uya_tramp_table, uya_tramp_count); } + +/* ---- logging ---- */ +static FILE *logfile; +void uya_log_open(void) +{ + char p[1024]; + snprintf(p, sizeof p, "%s/rac1_log.txt", plat_exe_dir()); + logfile = fopen(p, "w"); + if (logfile) fprintf(stderr, "[uya] log file: %s\n", p); +} + +void uya_log(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); fprintf(stderr, "[uya] "); vfprintf(stderr, fmt, ap); fputc('\n', stderr); va_end(ap); + fflush(stderr); + if (logfile) { + va_start(ap, fmt); fprintf(logfile, "[uya] "); vfprintf(logfile, fmt, ap); fputc('\n', logfile); va_end(ap); + fflush(logfile); + } +} + +#define MAXU 4096 +static struct { const char *name; uint32_t addr; unsigned count; } unimpl[MAXU]; +static int nunimpl; +static unsigned long long total_calls; + +static int bump(const char *name, uint32_t addr) +{ + total_calls++; + for (int i = 0; i < nunimpl; i++) + if ((name && unimpl[i].name == name) || (!name && unimpl[i].addr == addr)) return ++unimpl[i].count; + if (nunimpl < MAXU) { unimpl[nunimpl].name = name; unimpl[nunimpl].addr = addr; unimpl[nunimpl].count = 1; nunimpl++; } + return 1; +} + +void uya_unimpl(const char *name) +{ + if (bump(name, 0) == 1) uya_log("unimplemented game function %s", name); +} + +static unsigned long long trap_limit = 2000000; +void uya_trap_call(uint32_t ps2_addr, uint32_t ret_addr, const uint32_t *args) +{ + if (bump(0, ps2_addr) == 1) + uya_log("unimplemented call to PS2 0x%08X (from host 0x%08X) args %08X %08X %08X %08X", + ps2_addr, ret_addr, args[0], args[1], args[2], args[3]); + (void)trap_limit; +} + +void uya_report(void) +{ + static int done; + if (done++) return; + uya_log("---- %d distinct unimplemented functions, %llu calls ----", nunimpl, total_calls); + for (int i = 0; i < nunimpl; i++) { + if (unimpl[i].name) uya_log(" %8u %s", unimpl[i].count, unimpl[i].name); + else uya_log(" %8u PS2 0x%08X", unimpl[i].count, unimpl[i].addr); + } +} diff --git a/games/rac1/host/src/sdk/cdvd.c b/games/rac1/host/src/sdk/cdvd.c new file mode 100644 index 0000000..fad6b90 --- /dev/null +++ b/games/rac1/host/src/sdk/cdvd.c @@ -0,0 +1,128 @@ +/* Disc access: the engine core's CD read path, served from your own disc image. + * + * The game reads 2048-byte sectors by LSN. boot_elf reads the table of + * contents (16 sectors at LSN 1001) into 0x160C40 before it starts the menu + * (uyafn_13D2E0); uya_disc_boot() does the same. Reads finish immediately. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include +#include + +static FILE *disc; +static void (*cd_callback)(s32); /* sceCdCallback-style completion handler */ +static int cd_pending; /* a read finished; its callback is still owed */ +static unsigned long long nreads, nsectors; + +#ifdef _WIN32 +int _fseeki64(FILE *, long long, int); +#define SEEK64(f, o) _fseeki64(f, (long long)(o), SEEK_SET) +#else +#define SEEK64(f, o) fseeko(f, (off_t)(o), SEEK_SET) +#endif + +int uya_disc_open(const char *path) +{ + disc = fopen(path, "rb"); + if (!disc) { uya_log("cannot open disc image %s", path); return -1; } + uya_log("disc image: %s", path); + return 0; +} + +static int read_sectors_q(u32 lsn, u32 count, void *buf, int quiet); +static int read_sectors(u32 lsn, u32 count, void *buf) { return read_sectors_q(lsn, count, buf, 0); } +static int read_sectors_q(u32 lsn, u32 count, void *buf, int quiet) +{ + if (!disc) { uya_log("disc read LSN %u x%u: no disc image", lsn, count); return -1; } + if (!quiet && ((u32)(uintptr_t)buf < PS2_RAM_LO || (u32)(uintptr_t)buf + count * 2048 > PS2_RAM_HI)) + uya_log("disc read LSN %u x%u into 0x%08X (outside PS2 RAM)", lsn, count, (u32)(uintptr_t)buf); + /* Testing on a partial copy of the disc: UYA_DISC_MAP names a file with one + * byte per sector (1 = present). Reads of absent sectors are logged and + * appended to .missing as "lsn count" so they can be fetched. */ + { + static FILE *map; static int tried; + if (!tried++) { const char *m = getenv("UYA_DISC_MAP"); if (m) map = fopen(m, "rb"); } + if (map) { + u32 miss = 0, first = 0; + for (u32 i = 0; i < count; i++) { + int c = 0; + if (fseek(map, (long)(lsn + i), SEEK_SET) == 0) c = fgetc(map); + if (c != 1) { if (!miss) first = lsn + i; miss++; } + } + if (miss) { + char p[1100]; snprintf(p, sizeof p, "%s.missing", getenv("UYA_DISC_MAP")); + FILE *f = fopen(p, "a"); if (f) { fprintf(f, "%u %u\n", lsn, count); fclose(f); } + uya_log("disc read LSN %u x%u: %u sectors not in the partial disc copy (first %u)", lsn, count, miss, first); + } + } + } + if (SEEK64(disc, (unsigned long long)lsn * 2048) != 0) return -1; + size_t got = fread(buf, 2048, count, disc); + nreads++; nsectors += count; + if ((nreads <= 64 && !quiet) || getenv("UYA_DISCLOG")) uya_log("disc read LSN %u x%u -> 0x%08X", lsn, count, (u32)(uintptr_t)buf); + return got == count ? 0 : -1; +} + +int uya_disc_read(uint32_t lsn, uint32_t count, void *buf) { return read_sectors_q(lsn, count, buf, 1); } + +/* 0x0013CD28: start a read of `count` sectors at `lsn` into `buf` (mode in the 4th argument). 1 = started. */ +s32 uyafn_13CD28(s32 lsn, s32 count, void *buf, void *mode) +{ + (void)mode; + if (read_sectors((u32)lsn, (u32)count, buf) != 0) return 0; + cd_pending = 1; /* on the PS2 the completion interrupt comes later, after the caller marks the read busy */ + return 1; +} + +/* Deliver a finished read's callback. Called from the functions the game polls. */ +void uya_cd_service(void) +{ + if (cd_pending) { + cd_pending = 0; + if (cd_callback) cd_callback(1); /* 1 = read finished */ + } +} + +/* 0x0013CDF0: read status; 0 = idle (with 1: poll, with 0: wait). */ +s32 uyafn_13CDF0(s32 block) { (void)block; uya_cd_service(); return 0; } + +/* 0x0013CF40: set the read-completion callback, returns the previous one. */ +void *uyafn_13CF40(void (*cb)(s32)) { void *old = (void *)cd_callback; cd_callback = cb; return old; } + +/* 0x0013CEF8: last read error; 0 = none. */ +s32 uyafn_13CEF8(void) { return 0; } + +/* 0x0013D290: synchronous read. */ +s32 uyafn_13D290(s32 lsn, s32 count, void *buf) +{ + read_sectors((u32)lsn, (u32)count, buf); + return 0; +} + +/* 0x0013D2E0: read the table of contents into 0x160C40. */ +s32 uyafn_13D2E0(void) +{ + return uyafn_13D290(0x3E9, 0x10, (void *)0x160C40); +} + +void uya_disc_boot(void) +{ + uyafn_13D2E0(); + uya_log("table of contents loaded at 0x00160C40"); +} + +/* 0x0013D310: load the next level's part of the table of contents (boot_elf). + * Per level there are three disc locations (TOC + 0x7400/0x7408/0x7410 + + * level * 0x18); each is read into `buf` and copied into place: 0x60 bytes to + * TOC + 0x79A0, 0x1818 bytes to TOC + 0x7A00 and 0x26F0 bytes to 0x00169E58. */ +s32 uyafn_13D310(s32 level, u8 *buf, s32 x) +{ + u8 *toc = (u8 *)0x00160C40; + u8 *e = toc + level * 0x18; + (void)x; + if (uya_disc_read(*(u32 *)(e + 0x7408), 1, buf) == 0) memcpy(toc + 0x79A0, buf, 0x60); + if (uya_disc_read(*(u32 *)(e + 0x7400), 4, buf) == 0) memcpy(toc + 0x7A00, buf, 0x1818); + if (uya_disc_read(*(u32 *)(e + 0x7410), 5, buf) == 0) memcpy((u8 *)0x00169E58, buf, 0x26F0); + uya_log("level %d: table of contents loaded", level); + return 0; +} diff --git a/games/rac1/host/src/sdk/dma.c b/games/rac1/host/src/sdk/dma.c new file mode 100644 index 0000000..5f3cde2 --- /dev/null +++ b/games/rac1/host/src/sdk/dma.c @@ -0,0 +1,270 @@ +#include +/* libdma and the EE DMA controller, as far as the menu needs them. + * + * sceDmaSend walks the source chain (the DMAtag list) the menu built and + * hands each block of data to uya_dma_sink(); the GS renderer will take its + * packets from there. Tags with the IRQ bit, and the end of the chain, raise + * the channel's DMAC interrupt: the CHCR TAG field holds the last tag's + * upper 16 bits, the way the menu's handlers expect. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include + +/* EE DMA channel register blocks: CHCR at +0x00, MADR +0x10, QWC +0x20, TADR +0x30. */ +static const u32 chan_base[10] = { + 0x10008000, 0x10009000, 0x1000A000, 0x1000B000, 0x1000B400, + 0x1000C000, 0x1000C400, 0x1000C800, 0x1000D000, 0x1000D400, +}; + +/* ---- DMAC interrupt handlers (kernel AddDmacHandler / EnableDmac) ---- */ +typedef s32 (*dmac_handler_t)(s32 ch); +static dmac_handler_t handlers[16]; +static u32 enabled; + +/* 0x0011EB30: AddDmacHandler(channel, handler, next): returns a handler id. */ +s32 uyafn_11EB30(s32 ch, dmac_handler_t h, s32 next) +{ + (void)next; + if (ch >= 0 && ch < 16) handlers[ch] = h; + return ch + 1; +} + +/* 0x0011F9A8: EnableDmac(channel) */ +s32 uyafn_11F9A8(s32 ch) { if (ch >= 0 && ch < 16) enabled |= 1u << ch; return 0; } + +static int walking[10]; /* a chain walk is running on the channel (its IRQ handler may set STR) */ +static int irq_handled(s32 ch) { return ch >= 0 && ch < 16 && (enabled & (1u << ch)) && handlers[ch]; } + +/* RemoveDmacHandler / DisableDmac */ +s32 uya_dmac_remove(s32 ch) { if (ch >= 0 && ch < 16) handlers[ch] = 0; return 0; } +s32 uya_dmac_disable(s32 ch) { if (ch >= 0 && ch < 16) enabled &= ~(1u << ch); return 0; } + +static void raise_irq(s32 ch) +{ + if (ch >= 0 && ch < 16 && (enabled & (1u << ch)) && handlers[ch]) handlers[ch](ch); +} + +/* 0x0012D650: sceDmaGetChan(id) */ +void *uyafn_12D650(s32 id) +{ + if (id < 0 || id > 9) return 0; + return (void *)(uintptr_t)chan_base[id]; +} + +static u32 zero_qw[4 * 4096]; +static void *phys(u32 a) +{ + /* host memory (the recompiled code's stack, host buffers): as is */ + if (a >= 0x40000000u && (a >> 28) != 7 && (a >> 31) == 0) return (void *)(uintptr_t)a; + if (a & 0x80000000u) return (void *)(uintptr_t)(PS2_SPR + (a & 0x3FF0)); /* scratchpad */ + a &= 0x01FFFFF0u; + if (a < PS2_RAM_LO) { /* kernel area: never valid for the game; read as zeros */ + static int warned; + if (!warned++) uya_log("DMA: chain points into the kernel area (0x%08X); reading zeros", a); + return zero_qw; + } + return (void *)(uintptr_t)a; +} + +unsigned long long nsends, nqw; + +/* Where transferred data goes: VIF1 data to the VIF decoder (host/src/gfx/vif.c). */ +void uya_vif1_data(const u32 *data, u32 qwc); +void uya_vif1_words(const u32 *data, u32 nwords); +void uya_gs_gif(const u32 *d, u32 qwc); +void uya_vif0_data(const u32 *data, u32 qwc); +void uya_vif0_words(const u32 *data, u32 nwords); +static void uya_dma_sink(s32 ch, const void *data, u32 qwc) +{ + nqw += qwc; + if (ch == 0) { uya_vif0_data(data, qwc); return; } + if (ch == 1) uya_vif1_data(data, qwc); + if (ch == 2) { extern const char *uya_gs_src; uya_gs_src = "PATH3 (DMA ch2)"; uya_gs_gif(data, qwc); } /* GIF channel: PATH3 */ +} + +static s32 chan_index(void *chan) +{ + for (s32 i = 0; i < 10; i++) if ((u32)(uintptr_t)chan == chan_base[i]) return i; + return -1; +} + +/* 0x0012D938: sceDmaSend(chan, tag): start a source-chain transfer from `tag`. */ +s32 uyafn_12D938(void *chan, void *tag) +{ + s32 ch = chan_index(chan); + volatile u32 *regs = (volatile u32 *)chan; + u32 t = (u32)(uintptr_t)tag, stack[2], sp = 0; + u32 tte = regs[0] & 0x40; /* CHCR TTE as the game set it */ + nsends++; + if (nsends <= 4 || (nsends & (nsends - 1)) == 0) + uya_log("DMA send #%llu on channel %d, chain at 0x%08X (%llu qwords so far)", nsends, ch, t, nqw); + for (int guard = 0; guard < 1000000; guard++) { + u32 *q = phys(t); + u32 lo = q[0], hi = q[1]; + u32 qwc = lo & 0xFFFF, id = (lo >> 28) & 7, irq = lo >> 31, addr = hi & 0x7FFFFFFF; + u32 next = 0; + int end = 0; + { + static long long dlog = -2; + if (dlog == -2) { const char *e = getenv("UYA_DMALOG"); dlog = e ? atoll(e) : -1; } + extern int uya_vif_log; + uya_vif_log = (long long)nsends == dlog; + if (uya_vif_log) uya_log(" DMA tag at 0x%08X: id %u qwc %u addr 0x%08X upper %08X %08X", t, id, qwc, addr, q[2], q[3]); + } + /* Tag transfer (CHCR TTE): the tag's upper 64 bits go to the VIF first + * (two VIF codes). The menu's VIF1 chains always carry VIF codes there + * (NOP, DIRECT), so it is on for channel 1. */ + if (ch == 1 && tte) uya_vif1_words(q + 2, 2); + switch (id) { + case 0: uya_dma_sink(ch, phys(addr), qwc); end = 1; break; /* refe */ + case 1: uya_dma_sink(ch, q + 4, qwc); next = t + 16 + qwc * 16; break; /* cnt */ + case 2: uya_dma_sink(ch, q + 4, qwc); next = addr; break; /* next */ + case 3: case 4: uya_dma_sink(ch, phys(addr), qwc); next = t + 16; break; /* ref, refs */ + case 5: uya_dma_sink(ch, q + 4, qwc); /* call */ + if (sp < 2) stack[sp++] = t + 16 + qwc * 16; + next = addr; break; + case 6: uya_dma_sink(ch, q + 4, qwc); /* ret */ + if (sp) next = stack[--sp]; else end = 1; + break; + case 7: uya_dma_sink(ch, q + 4, qwc); end = 1; break; /* end */ + } + if (irq || end) { + u32 tie = regs[0] & 0x80; + regs[0] = (lo & 0xFFFF0000u) | (regs[0] & 0xC1) | 0x4 | 0x100; /* CHCR: TAG, chain mode, STR */ + regs[12] = next ? next : t; /* TADR: the handlers ignore a zero tag address */ + if (irq && !end && tie && irq_handled(ch)) { + /* CHCR TIE: the DMAC stops at a tag with the IRQ bit; the + * channel's handler (RAC1's VU1 chain handler) restarts it + * by setting STR again. Without that the chain stays + * suspended until the game sets STR. */ + regs[0] &= ~0x100u; + walking[ch] = 1; + raise_irq(ch); + walking[ch] = 0; + if (!(regs[0] & 0x100)) { regs[8] = 0; return 0; } + t = regs[12]; + continue; + } + raise_irq(ch); + } + if (end) break; + t = next; + } + if (ch == 1) { + void uya_gfx_frame_stats(void); void uya_gif_stats(void); + static long long stop_after = -2; + if (stop_after == -2) { const char *e = getenv("UYA_FRAMES"); stop_after = e ? atoll(e) : -1; } + uya_gfx_frame_stats(); + if (nsends == 600) uya_gif_stats(); + if (stop_after > 0 && (long long)nsends >= stop_after) { + void uya_gs_stats(void); + const char *rd = getenv("UYA_RAMDUMP"); /* testing: save PS2 RAM (0x100000-0x2000000) */ + void vu_stats(void); + uya_log("UYA_FRAMES=%lld reached", stop_after); uya_gif_stats(); uya_gs_stats(); vu_stats(); + if (rd) { FILE *f = fopen(rd, "wb"); if (f) { fwrite((void *)0x100000, 1, 0x1F00000, f); fclose(f); } } + exit(0); + } + } + regs[0] &= ~0x100u; + regs[8] = 0; + return 0; +} + +/* RAC1 starts DMA by writing a channel's registers itself (CHCR with STR, + * bit 8), not only through sceDmaSend. Those registers are plain memory on + * the host, so uya_dma_poll() runs any channel whose STR bit is set: chain + * mode from TADR through the same walker as sceDmaSend, normal mode as one + * block of QWC quadwords at MADR. Channel 0 (VIF0) carries VU0 microcode + * uploads and data to the VIF0 decoder (host/src/gfx/vif.c). Called from the game's busy-wait + * helper, at every vsync and from the GS sync calls. */ +void uya_dma_kick(s32 ch); +void uya_dma_poll(void) +{ + static int busy; + if (busy) return; + busy = 1; + for (s32 ch = 0; ch < 10; ch++) uya_dma_kick(ch); + busy = 0; +} + +/* Scratchpad channels: 8 (fromSPR: SPR -> RAM) and 9 (toSPR: RAM -> SPR), normal mode; + * SADR at +0x80 is the scratchpad offset. */ +static void spr_transfer(s32 ch) +{ + volatile u32 *r = (volatile u32 *)(uintptr_t)chan_base[ch]; + u32 qwc = r[8] & 0xFFFF, madr = r[4], sadr = r[32] & 0x3FF0; + u8 *spr = (u8 *)(uintptr_t)PS2_SPR, *ram = phys(madr); + for (u32 i = 0; i < qwc; i++) { + u8 *s = spr + ((sadr + 16 * i) & 0x3FF0), *m = ram + 16 * i; + if (ch == 9) memcpy(s, m, 16); else memcpy(m, s, 16); + } + r[4] = madr + qwc * 16; r[32] = (sadr + qwc * 16) & 0x3FF0; r[8] = 0; +} + +/* run channel ch now if its STR bit is set */ +void uya_dma_kick(s32 ch) +{ + volatile u32 *r = (volatile u32 *)(uintptr_t)chan_base[ch]; + u32 chcr = r[0]; + if (!(chcr & 0x100)) return; + if (ch < 10 && walking[ch]) return; /* restarted from its own IRQ handler: the walk goes on */ + u32 mode = (chcr >> 2) & 3; + if (ch == 1 || ch == 2) { + if (mode == 1) { uyafn_12D938((void *)(uintptr_t)chan_base[ch], (void *)(uintptr_t)r[12]); return; } + if (r[8]) uya_dma_sink(ch, phys(r[4]), r[8] & 0xFFFF); + } else if (ch == 0) { /* VIF0: VU0 microcode (MPG) and data */ + if (mode == 1) { + u32 t = r[12], stack[2], sp = 0; + for (int g = 0; g < 65536; g++) { + u32 *q = phys(t); u32 qwc = q[0] & 0xFFFF, id = (q[0] >> 28) & 7, addr = q[1] & 0x7FFFFFFF, next = 0; + int end = 0; + if (chcr & 0x40) uya_vif0_words(q + 2, 2); + switch (id) { + case 0: uya_dma_sink(0, phys(addr), qwc); end = 1; break; + case 1: uya_dma_sink(0, q + 4, qwc); next = t + 16 + qwc * 16; break; + case 2: uya_dma_sink(0, q + 4, qwc); next = addr; break; + case 3: case 4: uya_dma_sink(0, phys(addr), qwc); next = t + 16; break; + case 5: uya_dma_sink(0, q + 4, qwc); if (sp < 2) stack[sp++] = t + 16 + qwc * 16; next = addr; break; + case 6: uya_dma_sink(0, q + 4, qwc); if (sp) next = stack[--sp]; else end = 1; break; + default: uya_dma_sink(0, q + 4, qwc); end = 1; break; + } + if (end) break; + t = next; + } + } else if (r[8]) uya_dma_sink(0, phys(r[4]), r[8] & 0xFFFF); + } else if (ch == 8 || ch == 9) { + if (mode == 0) spr_transfer(ch); + else if (mode == 1) { /* chain mode toSPR: walk tags from TADR into the scratchpad */ + u32 t = r[12], sadr = r[32] & 0x3FF0; + for (int g = 0; g < 4096; g++) { + u32 *q = phys(t); u32 qwc = q[0] & 0xFFFF, id = (q[0] >> 28) & 7, addr = q[1] & 0x7FFFFFFF; + const u8 *src = (id == 1 || id == 7) ? (u8 *)(q + 4) : (u8 *)phys(addr); + for (u32 i = 0; i < qwc; i++) memcpy((u8 *)(uintptr_t)PS2_SPR + ((sadr + 16 * i) & 0x3FF0), src + 16 * i, 16); + sadr = (sadr + 16 * qwc) & 0x3FF0; + if (id == 7 || id == 0) break; + t = id == 1 ? t + 16 + qwc * 16 : (id == 2 ? addr : t + 16); + } + r[32] = sadr; + } + } + r[0] &= ~0x100u; + r[8] = 0; +} + +void rc_io_write32(u32 a, u32 v) +{ + *(volatile u32 *)(uintptr_t)a = v; + for (s32 ch = 0; ch < 10; ch++) + if (a == chan_base[ch] && (v & 0x100)) { uya_dma_kick(ch); return; } +} + +typedef union { u64 d[2]; u32 w[4]; } R128h; +void rc_io_write128(u32 a, const void *v) +{ + if (a == 0x10005000u) { uya_vif1_data((const u32 *)v, 1); return; } /* VIF1 FIFO */ + if (a == 0x10006000u) { extern const char *uya_gs_src; uya_gs_src = "PATH3 (FIFO)"; uya_gs_gif((const u32 *)v, 1); return; } /* GIF FIFO (PATH3) */ + if (a == 0x10004000u) { uya_vif0_data((const u32 *)v, 1); return; } /* VIF0 FIFO */ + const u32 *w = v; + for (int i = 0; i < 4; i++) rc_io_write32(a + 4 * i, w[i]); +} diff --git a/games/rac1/host/src/sdk/graph.c b/games/rac1/host/src/sdk/graph.c new file mode 100644 index 0000000..0f2e35e --- /dev/null +++ b/games/rac1/host/src/sdk/graph.c @@ -0,0 +1,59 @@ +/* libgraph: vsync and the EE timer the menu measures frames with. */ +#include "uya_host.h" +#include "uya_runtime.h" +#include + +#define T0_COUNT (*(volatile u32 *)0x10000800) /* EE timer 0, BUSCLK/256 = 576000 Hz */ +/* the length of a field: NTSC 59.94 Hz until the game sets the video mode + * (sceGsResetGraph, host/src/sdk/gsdk.c); RAC1 (PAL) runs at 50 Hz */ +static uint32_t frame_us = 16683; +void uya_set_video_pal(int pal) { frame_us = pal ? 20000 : 16683; } +#define FRAME_US frame_us +uint32_t uya_frame_us(void) { return frame_us; } + +static uint64_t last, next_vsync; +static u32 field; + +/* 0x0012C908: sceGsSyncV(mode): wait for the next vertical blank, return the field (0/1). + * Paces the game at the video mode's field rate (50 Hz PAL, 59.94 Hz NTSC) and + * advances timer 0 by the time that really passed. */ +void uya_gs_vsync(void); +void uya_sound_pump(void); +void uya_mc_trace(void); +void uya_call_vsync_callback(void); +void uya_dma_poll(void); +void rac1_cd_service(void); +void rac1_test_hooks(void); +static uint32_t vsyncs; +uint32_t uya_frame_count(void) { return vsyncs; } +/* testing: UYA_FIXEDTIME=1 puts the game on a clock that moves a field per + * vsync (timer 0 here, the Linux sound output's pacing) and never waits, so a + * run does the same whatever the speed of the machine (frames can be compared + * between builds) */ +static int fixed_time = -1; +static uint64_t vclock; +uint64_t uya_fixed_clock(void) { return fixed_time > 0 ? vclock : 0; } + +s32 uyafn_12C908(s32 mode) +{ + vsyncs++; + if (fixed_time < 0) fixed_time = getenv("UYA_FIXEDTIME") != 0; + uint64_t now = fixed_time ? (vclock += FRAME_US) : plat_time_us(); + (void)mode; + if (!next_vsync) { next_vsync = now + FRAME_US; last = now; } + if (now < next_vsync && !fixed_time) { plat_sleep_us(next_vsync - now); now = plat_time_us(); } + next_vsync += FRAME_US; + if (now > next_vsync + 4 * FRAME_US) next_vsync = now + FRAME_US; /* fell far behind: resync */ + T0_COUNT += (u32)((now - last) * 576000u / 1000000u); + last = now; + field ^= 1; + uya_gs_vsync(); + uya_sound_pump(); + uya_mc_trace(); + uya_dma_poll(); + rac1_cd_service(); + uya_call_vsync_callback(); + rac1_test_hooks(); + plat_poll(); + return (s32)field; +} diff --git a/games/rac1/host/src/sdk/gsdk.c b/games/rac1/host/src/sdk/gsdk.c new file mode 100644 index 0000000..4cc6cd1 --- /dev/null +++ b/games/rac1/host/src/sdk/gsdk.c @@ -0,0 +1,171 @@ +/* libgraph (Sony's GS library) functions the menu calls, plus boot_elf's + * game-state word. Ported from the decomp's boot_elf sources and assembly + * (src/boot_elf/core/12A948.c, asm/boot_elf/nonmatchings/core). */ +#include "uya_host.h" +#include "uya_runtime.h" +#include + +void uya_gs_gif(const u32 *d, u32 qwc); + +typedef struct { s16 inter, omode, ffmode, version; } GParam; + +/* 0x0012C078: sceGsGetGParam(); the table is boot_elf data (interlace, NTSC, frame mode). */ +/* RAC1: libgraph's GParam is D_00132E40 (func_00121D08). */ +void *uyafn_12C078(void) { return (void *)0x00132E40; } + +/* 0x0012C138: sceGsSetDefDispEnv(disp, psm, w, h, dx, dy) for NTSC and PAL */ +void uyafn_12C138(u64 *disp, s32 psm, s32 w, s32 h, s32 dx, s32 dy) +{ + GParam *gp = uyafn_12C078(); + s16 sw = (s16)w, sh = (s16)h; + disp[0] = 0x66; /* PMODE: circuit 2, ALP 0 */ + disp[1] = gp->inter == 0 ? 2 : (gp->ffmode ? 3 : 1); /* SMODE2 */ + disp[2] = ((u64)(psm & 0xF) << 15) | ((u64)(((sw + 63) >> 6) & 0x3F) << 9); /* DISPFB */ + if ((gp->omode == 2 || gp->omode == 3) && sw > 0) { + s32 ntsc = gp->omode == 2; + s32 magh = (sw + 0x9FF) / sw; + u64 x = (u64)((dx * magh + (ntsc ? 0x27C : 0x290)) & 0xFFF); + u64 y = (u64)(((s16)dy + (ntsc ? 0x32 : 0x48)) & 0xFFF); + u64 dh = (u64)((gp->inter == 1 && gp->ffmode) ? sh * 2 - 1 : sh - 1); + if (gp->inter != 1) y = (u64)(((s16)dy + (ntsc ? 0x19 : 0x24)) & 0xFFF), dh = (u64)(sh - 1); + disp[3] = x | (y << 12) | ((u64)(magh - 1) << 23) | ((u64)(magh * sw - 1) << 32) | (dh << 44); + } else if (gp->omode == 0x50 && sw > 0) { /* DTV 480p (progressive scan) */ + u64 x = (u64)((((0x2D0 - sw) / 2) * 2 + dx * 2 + 0xE8) & 0xFFF); + u64 y = (u64)(((s16)dy + 0x23) & 0xFFF); + disp[3] = x | (y << 12) | (1ull << 23) | ((u64)(sw * 2 - 1) << 32) | ((u64)(sh - 1) << 44); + } else { + disp[3] = 0; + } + disp[4] = 0; /* BGCOLOR */ +} + +/* 0x0012C4B0: sceGsPutDispEnv(disp) */ +void uyafn_12C4B0(u64 *disp) +{ + GParam *gp = uyafn_12C078(); + if (!disp) return; + if (gp->version == 1) { + *(volatile u64 *)0x12000000 = disp[0]; + *(volatile u64 *)0x12000070 = disp[2]; + *(volatile u64 *)0x12000080 = disp[3]; + *(volatile u64 *)0x120000C0 = disp[4]; + } else { + *(volatile u64 *)0x12000000 = disp[0]; + *(volatile u64 *)0x12000020 = disp[1]; + *(volatile u64 *)0x12000090 = disp[2]; + *(volatile u64 *)0x120000A0 = disp[3]; + *(volatile u64 *)0x120000E0 = disp[4]; + } +} + +/* 0x0012C570: size of a frame buffer in pages */ +s16 uyafn_12C570(s16 psm, s16 w, s16 h) +{ + GParam *gp = uyafn_12C078(); + s32 bw = (w + 63) / 64, bh = (psm & 2) ? (h + 63) / 64 : (h + 31) / 32; + if (gp->inter == 1 && gp->ffmode == 0) return (s16)(bw * bh); + return (s16)(bw * bh * 2); +} + +/* 0x0012C638: sceGsSetDefDrawEnv(draw, psm, w, h, ztest, zpsm): 8 A+D pairs */ +s32 uyafn_12C638(u64 *d, s16 psm, s16 w, s16 h, s16 ztest, s16 zpsm) +{ + d[1] = 0x4C; d[0] = ((u64)(((w + 63) >> 6) & 0x3F) << 16) | ((u64)(psm & 0xF) << 24); + d[3] = 0x4E; d[2] = (u64)(u16)uyafn_12C570(psm, w, h) | ((u64)(zpsm & 0xF) << 24) | (ztest == 0 ? 1ull << 32 : 0); + d[5] = 0x18; d[4] = ((u64)(0x800 - (s16)(w >> 1)) << 4) | ((u64)(0x800 - (s16)(h >> 1)) << 36); + d[7] = 0x40; d[6] = ((u64)(w - 1) << 16) | ((u64)(h - 1) << 48); + d[9] = 0x1A; d[8] = 1; + d[11] = 0x46; d[10] = 1; + d[13] = 0x45; d[12] = (psm & 2) ? 1 : 0; + d[15] = 0x47; d[14] = ztest ? (((u64)(ztest & 3) << 17) | 0x10000) : 0x30000; + return 8; +} + +/* 0x0012C9A0: sceGsSetDefLoadImage(lp, dbp, dbw, dpsm, x, y, w, h): a 6-qword + * GIF packet (BITBLTBUF, TRXPOS, TRXREG, TRXDIR, then an IMAGE tag). */ +s32 uyafn_12C9A0(u64 *lp, s32 dbp, s32 dbw, s32 dpsm, s32 x, s32 y, s32 w, s32 h) +{ + s16 p = (s16)dpsm, W = (s16)w, H = (s16)h; + s32 qwc; + /* libgraph's table (RAC1 0x001531E0): PSMT8H and PSMT4HL/HH are 8- and + * 4-bit transfers too (the HUD's PSMT8H textures, re-sent when the pause + * menu closes); a format it does not know sends no image */ + switch (p) { + case 0x00: case 0x30: qwc = W * H / 4; break; + case 0x01: case 0x31: qwc = W * H * 3 / 16; break; + case 0x02: case 0x0A: case 0x32: case 0x3A: qwc = W * H / 8; break; + case 0x13: case 0x1B: qwc = W * H / 16; break; + case 0x14: case 0x24: case 0x2C: qwc = W * H / 32; break; + default: qwc = 0; break; + } + if (qwc > 0x7FFF) { uya_log("sceGsSetDefLoadImage: image too large"); return 0; } + lp[0] = 4 | 0x8000 | (1ull << 60); lp[1] = 0xE; + lp[2] = ((u64)(u16)(s16)dbp << 32) | ((u64)((s16)dbw & 0x3F) << 48) | ((u64)(p & 0x3F) << 56); lp[3] = 0x50; + lp[4] = ((u64)((s16)x & 0x7FF) << 32) | ((u64)((s16)y & 0x7FF) << 48); lp[5] = 0x51; + lp[6] = (u64)(W & 0xFFF) | ((u64)(H & 0xFFF) << 32); lp[7] = 0x52; + lp[8] = 0; lp[9] = 0x53; + lp[10] = (u64)(qwc & 0x7FFF) | 0x8000 | (2ull << 58); lp[11] = 0; + return 6; +} + +static const u32 *ps2_ptr(const void *p) +{ + u32 a = (u32)(uintptr_t)p; + /* PS2 uncached / accelerated mirrors of main RAM; anything else (main RAM, + * the scratchpad, or a host pointer such as a stack buffer) is used as is */ + if (a >= 0x20000000 && a < 0x40000000) return (const u32 *)(uintptr_t)(a & 0x0FFFFFFF); + return (const u32 *)p; +} + +/* 0x0012CCC8: sceGsExecLoadImage(lp, src): sends the packet then the pixels over GIF channel 2 */ +s32 uyafn_12CCC8(u64 *lp, void *src) +{ + { extern const char *uya_gs_src; uya_gs_src = "PATH3 (GS library)"; } + uya_gs_gif(ps2_ptr(lp), 6); + uya_gs_gif(ps2_ptr(src), (u32)(lp[10] & 0x7FFF)); + return 0; +} + +/* 0x0013D3C0 / 0x0013D3C8: set / get a game-state word in boot_elf (gp-relative). + * The menu's front loop uses it for its own states (0 = title, 5 = attract demo...). */ +static s32 game_state; +void uyafn_13D3C0(s32 v) { game_state = v; } +s32 uyafn_13D3C8(void) { return game_state; } + +/* 0x0012BEE8: sceGsResetGraph(mode, inter, omode, ffmode). Mode 0 resets the + * GS and sets the video mode, mode 5 only sets the mode, mode 1 only resets. + * The host keeps the mode in the GParam table (sceGsGetGParam) and in SMODE2, + * which is what the display code reads; there is no CRT to program. */ +void uyafn_12BEE8(s32 mode, s32 inter, s32 omode, s32 ffmode) +{ + GParam *gp = uyafn_12C078(); + if (mode != 0 && mode != 5) return; + gp->inter = (s16)inter; + gp->omode = (s16)omode; + gp->ffmode = ffmode != 0; + *(volatile u64 *)0x12000020 = (u64)((inter & 1) | ((ffmode & 1) << 1)); + { void uya_set_video_pal(int pal); uya_set_video_pal((omode & 0xFF) == 3); } /* vsync pacing (graph.c) */ + uya_log("video mode: %s, %s%s", (omode & 0xFF) == 0x50 ? "480p" : (omode & 0xFF) == 3 ? "PAL" : (omode & 0xFF) == 2 ? "NTSC" : "other", + inter & 1 ? "interlaced" : "non-interlaced", ffmode & 1 ? " frame mode" : ""); +} + +/* 0x0012A950: sceGsResetPath(): resets VIF1, VU1 and the GIF. Nothing to reset here. */ +void uyafn_12A950(void) {} + +/* 0x0012D4D8: sceGsSyncVCallback(func): installs a vblank handler and returns + * the previous one. The menu passes boot_elf's handler at 0x0013D208, which + * only feeds boot_elf's own timing; the host keeps time itself, so the handler + * is recorded and not called. */ +/* RAC1: the game's handler (vsync_callback, 0x0012F308) counts frames and + * reads timer 0, so it is called at every vsync (host/src/sdk/graph.c). */ +static s32 vsync_cb; +s32 uyafn_12D4D8(s32 fn) { s32 old = vsync_cb; vsync_cb = fn; return old; } +void uya_call_vsync_callback(void) +{ + if (vsync_cb) { typedef s32 (*cb_t)(s32); ((cb_t)(uintptr_t)(u32)vsync_cb)(0); } +} + +/* 0x0012A9F0: sceGsSyncPath(mode, timeout): waits for DMA, VIF1 and GIF to go + * idle and returns 0. Host transfers finish when they are sent, so the paths + * are always idle. */ +s32 uyafn_12A9F0(s32 mode, s32 timeout) { (void)mode; (void)timeout; return 0; } diff --git a/games/rac1/host/src/sdk/libc.c b/games/rac1/host/src/sdk/libc.c new file mode 100644 index 0000000..0094f4f --- /dev/null +++ b/games/rac1/host/src/sdk/libc.c @@ -0,0 +1,124 @@ +/* Engine core (boot_elf) C library functions the menu calls, implemented on the host. + * Each is named by its PS2 address so the menu's calls and function pointers reach it. */ +#include "uya_host.h" + +/* 0x0011A264: memset(dst, c, n) (newlib) */ +void *uyafn_11A264(void *dst, s32 c, u32 n) +{ + u8 *p = dst; + while (n--) *p++ = (u8)c; + return dst; +} + +#include +int vprintf(const char *, va_list); + +/* 0x00121760: printf(fmt, ...) */ +int uyafn_121760(const char *fmt, ...) +{ + va_list ap; + int n; + va_start(ap, fmt); + n = vprintf(fmt, ap); + va_end(ap); + return n; +} + +/* 0x0011F0A0: FlushCache(mode). Nothing to do on x86. */ +s32 uyafn_11F0A0(s32 mode) { (void)mode; return 0; } + +void uya_log(const char *fmt, ...); +/* 0x00116F98: __assert(file, line, expression) (newlib). Logged; the game carries on. */ +void uyafn_116F98(const char *file, s32 line, const char *expr) +{ + uya_log("ASSERT %s:%d: %s", file ? file : "?", line, expr ? expr : "?"); +} + +int vsprintf(char *, const char *, va_list); +/* 0x0011B2E8: sprintf(buf, fmt, ...) */ +int uyafn_11B2E8(char *buf, const char *fmt, ...) +{ + va_list ap; + int n; + va_start(ap, fmt); + n = vsprintf(buf, fmt, ap); + va_end(ap); + return n; +} + +/* 0x00116FD0: atexit(fn). The menu registers cleanup it never needs here. */ +s32 uyafn_116FD0(void (*fn)(void)) { (void)fn; return 0; } + +/* 0x0011BB58: strncpy (newlib, an MMI-optimised version on the EE). */ +char *uyafn_11BB58(char *dst, const char *src, u32 n) +{ + u32 i = 0; + for (; i < n && src[i]; i++) dst[i] = src[i]; + for (; i < n; i++) dst[i] = 0; + return dst; +} + +/* 0x0013D3E0: the game's rand(): seed (0x001D4D80) = seed * 0x343FD + 0x269EC3, + * returns bits 16-30. */ +s32 uyafn_13D3E0(void) +{ + u32 *seed = (u32 *)0x001D4D80; + *seed = *seed * 0x343FDu + 0x269EC3u; + return (s32)((*seed >> 16) & 0x7FFF); +} + +/* 0x00124ED8: LoadExecPS2(path, argc, argv): the menu starts another + * executable (Multiplayer starts cdrom0:\I5BOOTN.ELF, the online game). The + * wrapper has only the menu, so it says so and stops. */ +void uya_log(const char *fmt, ...); +void exit(int); +void uyafn_124ED8(const char *path, s32 argc, const char **argv) +{ + (void)argc; (void)argv; + uya_log("the menu now starts %s (another program on the disc); the wrapper only has the menu, so it stops here.", path ? path : "?"); + exit(0); +} +/* 0x0012EF10, 0x00121B38: shut down the IOP link before LoadExecPS2; nothing to do. */ +void uyafn_12EF10(void) {} +void uyafn_121B38(void) {} +/* 0x0013B0C8: hand the boot options string to the next program; nothing to do. */ +void uyafn_13B0C8(void *p) { (void)p; } + +/* 0x0011A0B0: memcpy(dst, src, n) */ +void *uyafn_11A0B0(void *dst, const void *src, u32 n) +{ + u8 *d = dst; const u8 *s = src; + while (n--) *d++ = *s++; + return dst; +} + +/* 0x0011B868: strlen(s) */ +u32 uyafn_11B868(const char *s) { u32 n = 0; while (s[n]) n++; return n; } + +/* 0x0011B754: strcpy(dst, src) */ +char *uyafn_11B754(char *dst, const char *src) { char *d = dst; while ((*d++ = *src++)); return dst; } + +/* 0x0011B610: strcmp(a, b) */ +s32 uyafn_11B610(const char *a, const char *b) +{ + while (*a && *a == *b) a++, b++; + return (u8)*a - (u8)*b; +} + +/* 0x0011B9A0: strncmp(a, b, n) */ +s32 uyafn_11B9A0(const char *a, const char *b, u32 n) +{ + for (; n; n--, a++, b++) { if (*a != *b) return (u8)*a - (u8)*b; if (!*a) return 0; } + return 0; +} + +/* 0x0011AF48: printf(fmt, ...) through newlib's stdout (another entry to the same output) */ +int uyafn_11AF48(const char *fmt, ...) +{ + va_list ap; + int n; + va_start(ap, fmt); + n = vprintf(fmt, ap); + va_end(ap); + return n; +} diff --git a/games/rac1/host/src/sdk/memcard.c b/games/rac1/host/src/sdk/memcard.c new file mode 100644 index 0000000..c111fde --- /dev/null +++ b/games/rac1/host/src/sdk/memcard.c @@ -0,0 +1,373 @@ +/* libmc (Sony's memory card library) on the host, backed by folders. + * + * The menu talks to the card the way every PS2 game does: it starts a command + * (sceMcGetInfo, sceMcOpen, sceMcRead, ...) which returns 0 when accepted, then + * polls sceMcSync each frame until it reports the command finished, and reads + * the command's result from there. Here each command runs at once on a folder + * and its result waits for the next sceMcSync. + * + * Memory card slot 1 (port 0) is the folder "memcard/slot1" next to the + * program; slot 2 (port 1) is "memcard/slot2" and only counts as inserted when + * that folder exists. Inside a slot folder each save is a folder named like + * the card's directory (BASCUS-97353RATCHET), with the save's files in it. + * + * Result codes are libmc's: 0 success, -1 card changed, -2 unformatted, + * -3 card full, -4 no such file or directory (or it already exists, for + * mkdir), -5 permission denied, -6 directory not empty, -7 too many open files, + * -10 no card. Addresses are named by the boot_elf functions they replace + * (src/boot_elf/core/12A948.c). */ +#include "uya_host.h" +#include "uya_runtime.h" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#define mkdir_one(p) _mkdir(p) +#define rmdir_one(p) _rmdir(p) +#else +#include +#define mkdir_one(p) mkdir(p, 0777) +#define rmdir_one(p) rmdir(p) +#endif + +#define CARD_KB 8192 /* an 8 MB card, counted in 1 KB clusters */ + +static s32 pend_cmd, pend_res; /* the finished command waiting for sceMcSync */ +static int seen[2]; /* GetInfo reported this card already (else "new card") */ + +/* UYA_MCLOG=1 logs card commands (a command repeating its last result, as the + * menu's sceMcGetInfo polling does, is logged once) and the card manager trace + * below. In PowerShell: $env:UYA_MCLOG=1; in cmd: set UYA_MCLOG=1. */ +static int mclog(void) { static int v = -1; if (v < 0) v = getenv("UYA_MCLOG") != 0; return v; } + +static void finish(s32 cmd, s32 res) +{ + static s32 last_cmd = -1, last_res = 0x7FFFFFFF; + pend_cmd = cmd; pend_res = res; + if (mclog() && (cmd != last_cmd || res != last_res)) + uya_log("memory card: command 0x%X result %d", cmd, res); + last_cmd = cmd; last_res = res; +} + +static const char *slot_dir(int port) +{ + static char d[2][1024]; + snprintf(d[port & 1], sizeof d[0], "%s/memcard/slot%d", plat_exe_dir(), (port & 1) + 1); + return d[port & 1]; +} + +static int is_dir(const char *p) { struct stat st; return stat(p, &st) == 0 && S_ISDIR(st.st_mode); } + +/* File size by seeking: st_size from stat() reads as 0 in this 32-bit build. */ +static long file_size(const char *p) +{ + FILE *f = fopen(p, "rb"); + long n = 0; + if (f) { fseek(f, 0, SEEK_END); n = ftell(f); fclose(f); } + return n; +} + +static int card_present(int port) +{ + if (port & 1) return is_dir(slot_dir(1)); + /* slot 1 always has a card: make its folder on first use */ + char base[1024]; + snprintf(base, sizeof base, "%s/memcard", plat_exe_dir()); + mkdir_one(base); + mkdir_one(slot_dir(0)); + return is_dir(slot_dir(0)); +} + +/* '*' any run of characters, '?' one character */ +static int glob_match(const char *p, const char *s) +{ + if (!*p) return !*s; + if (*p == '*') { + for (; ; s++) { if (glob_match(p + 1, s)) return 1; if (!*s) return 0; } + } + if (!*s) return 0; + if (*p == '?' || *p == *s) return glob_match(p + 1, s + 1); + return 0; +} + +/* Card path ("/DIR/file" or "DIR/file") to a host path. Wildcards in the + * directory part pick the first directory that matches. Returns 0 if a + * wildcard directory had no match. */ +static int host_path(int port, const char *name, char *out, size_t n, char *last, size_t nlast) +{ + char tmp[512], cur[1024]; + snprintf(cur, sizeof cur, "%s", slot_dir(port)); + snprintf(tmp, sizeof tmp, "%s", name ? name : ""); + char *parts[16]; int np = 0; + for (char *t = strtok(tmp, "/"); t && np < 16; t = strtok(0, "/")) parts[np++] = t; + for (int i = 0; i < np - 1; i++) { + if (strpbrk(parts[i], "*?")) { + DIR *d = opendir(cur); + struct dirent *e; int found = 0; + while (d && (e = readdir(d))) + if (e->d_name[0] != '.' && glob_match(parts[i], e->d_name)) { + size_t l = strlen(cur); + snprintf(cur + l, sizeof cur - l, "/%s", e->d_name); found = 1; break; + } + if (d) closedir(d); + if (!found) return 0; + } else { + size_t l = strlen(cur); + snprintf(cur + l, sizeof cur - l, "/%s", parts[i]); + } + } + if (last) snprintf(last, nlast, "%s", np ? parts[np - 1] : ""); + if (np) { size_t l = strlen(cur); snprintf(cur + l, sizeof cur - l, "/%s", parts[np - 1]); } + snprintf(out, n, "%s", cur); + return 1; +} + +static long used_kb(const char *dir) +{ + long kb = 0; + DIR *d = opendir(dir); + struct dirent *e; + while (d && (e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char p[1100]; struct stat st; + snprintf(p, sizeof p, "%s/%s", dir, e->d_name); + if (stat(p, &st)) continue; + kb += S_ISDIR(st.st_mode) ? 1 + used_kb(p) : (file_size(p) + 1023) / 1024; + } + if (d) closedir(d); + return kb; +} + +/* 0x0012DA60 / 0x0012EA38: sceMcInit and friends. Nothing to set up. */ +s32 uyafn_12DA60(void) { return 0; } + +/* 0x0012E2E0: sceMcSync(mode, cmd*, result*): 1 = a command finished (its id + * and result are stored), -1 = no command pending. Commands finish at once, so + * it never reports "still running". */ +s32 uyafn_12E2E0(s32 mode, s32 *cmd, s32 *result) +{ + (void)mode; + if (!pend_cmd) return -1; + if (cmd) *cmd = pend_cmd; + if (result) *result = pend_res; + pend_cmd = 0; + return 1; +} + +/* 0x0012E400: sceMcGetInfo(port, slot, type*, free*, format*) */ +s32 uyafn_12E400(s32 port, s32 slot, s32 *type, s32 *free_kb, s32 *format) +{ + (void)slot; + static int told[2]; + if (!told[port & 1]) { told[port & 1] = 1; uya_log("memory card: slot %d is %s (%s)", (port & 1) + 1, slot_dir(port), card_present(port) ? "inserted" : "empty"); } + if (!card_present(port)) { + seen[port & 1] = 0; + if (type) *type = 0; + finish(1, -10); + return 0; + } + if (type) *type = 2; /* PS2 memory card */ + if (format) *format = 1; + if (free_kb) { long f = CARD_KB - 1 - used_kb(slot_dir(port)); *free_kb = f < 0 ? 0 : (s32)f; } + finish(1, seen[port & 1] ? 0 : -1); /* first look: "a formatted card was inserted" */ + seen[port & 1] = 1; + return 0; +} + +/* sceMcGetDir table entry (sceMcTblGetDir), 0x40 bytes */ +typedef struct { + u8 create[8], modify[8]; + u32 size; + u16 attr, r1; + u32 r2, pda; + char name[32]; +} McDirEnt; + +static void fill_ent(McDirEnt *e, const char *name, int dir, u32 size) +{ + memset(e, 0, sizeof *e); + e->create[2] = e->modify[2] = 0; /* sec, min, hour left zero */ + e->create[4] = e->modify[4] = 1; /* day 1 */ + e->create[5] = e->modify[5] = 1; /* month 1 */ + e->create[6] = e->modify[6] = 2004 & 0xFF; e->create[7] = e->modify[7] = 2004 >> 8; + e->size = size; + e->attr = dir ? 0x8427 : 0x8497; + snprintf(e->name, sizeof e->name, "%s", name); +} + +static struct { char dir[1024], pat[64]; int pos; } gd; + +static s32 list_dir(McDirEnt *out, s32 maxent) +{ + DIR *d = opendir(gd.dir); + if (!d) return -4; + struct dirent *e; + int idx = 0, n = 0, top = !strcmp(gd.dir, slot_dir(0)) || !strcmp(gd.dir, slot_dir(1)); + /* a subdirectory lists "." and ".." first, like the card does */ + if (!top) { + const char *dots[2] = {".", ".."}; + for (int k = 0; k < 2; k++) + if (glob_match(gd.pat, dots[k])) { + if (idx++ >= gd.pos && n < maxent) fill_ent(&out[n++], dots[k], 1, 0); + } + } + while ((e = readdir(d)) && n < maxent) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + if (!glob_match(gd.pat, e->d_name)) continue; + char p[1200]; struct stat st; + snprintf(p, sizeof p, "%s/%s", gd.dir, e->d_name); + if (stat(p, &st)) continue; + if (idx++ < gd.pos) continue; + fill_ent(&out[n++], e->d_name, S_ISDIR(st.st_mode), S_ISDIR(st.st_mode) ? 0 : (u32)file_size(p)); + } + closedir(d); + gd.pos += n; + return n; +} + +/* 0x0012E580: sceMcGetDir(port, slot, name, mode, maxent, table): mode 0 + * starts a listing of name (wildcards allowed in its last part), mode 1 + * continues it. The result is the number of entries written. */ +s32 uyafn_12E580(s32 port, s32 slot, char *name, s32 mode, s32 maxent, void *table) +{ + (void)slot; + if (!card_present(port)) { finish(0xD, -10); return 0; } + if (mclog()) uya_log("memory card: list %s mode %d max %d", name, mode, maxent); + if (mode == 0) { + char last[64]; + if (!host_path(port, name, gd.dir, sizeof gd.dir, last, sizeof last)) { finish(0xD, 0); gd.dir[0] = 0; return 0; } + char *sl = strrchr(gd.dir, '/'); + if (sl) *sl = 0; /* gd.dir = the folder, gd.pat = the pattern in it */ + snprintf(gd.pat, sizeof gd.pat, "%s", last[0] ? last : "*"); + gd.pos = 0; + } + if (!gd.dir[0]) { finish(0xD, 0); return 0; } + s32 r = list_dir((McDirEnt *)table, maxent); + finish(0xD, r == -4 ? 0 : r); + return 0; +} + +static FILE *fds[8]; + +/* 0x0012DCA0: sceMcOpen(port, slot, name, mode): mode 1 read, 2 write, 3 both, + * 0x200 create, 0x40 make a directory (what sceMcMkdir sends). Result: the + * file descriptor, or an error. */ +s32 uyafn_12DCA0(s32 port, s32 slot, char *name, s32 mode) +{ + (void)slot; + char p[1024]; + s32 cmd = (mode & 0x40) ? 0xB : 2; + if (!card_present(port)) { finish(cmd, -10); return 0; } + if (mclog()) uya_log("memory card: open %s mode 0x%X", name, mode); + if (!host_path(port, name, p, sizeof p, 0, 0)) { finish(cmd, -4); return 0; } + if (mode & 0x40) { + if (is_dir(p)) { finish(cmd, -4); return 0; } + finish(cmd, mkdir_one(p) == 0 ? 0 : -4); + return 0; + } + int fd; + for (fd = 0; fd < 8 && fds[fd]; fd++); + if (fd == 8) { finish(cmd, -7); return 0; } + struct stat st; + int exists = stat(p, &st) == 0; + if (exists && S_ISDIR(st.st_mode)) { finish(cmd, -5); return 0; } + if (!exists && !(mode & 0x200)) { finish(cmd, -4); return 0; } + FILE *f = fopen(p, exists ? ((mode & 2) ? "r+b" : "rb") : "w+b"); + if (!f) { finish(cmd, -4); return 0; } + fds[fd] = f; + finish(cmd, fd); + return 0; +} + +/* 0x0012DDC8: sceMcMkdir(port, slot, name) */ +s32 uyafn_12DDC8(s32 port, s32 slot, char *name) { return uyafn_12DCA0(port, slot, name, 0x40); } + +/* 0x0012DE00: sceMcClose(fd) */ +s32 uyafn_12DE00(s32 fd) +{ + if (fd < 0 || fd >= 8 || !fds[fd]) { finish(3, -4); return 0; } + fclose(fds[fd]); fds[fd] = 0; + finish(3, 0); + return 0; +} + +/* 0x0012DEC0: sceMcSeek(fd, offset, whence 0/1/2): result is the new position */ +s32 uyafn_12DEC0(s32 fd, s32 off, s32 whence) +{ + if (fd < 0 || fd >= 8 || !fds[fd]) { finish(4, -4); return 0; } + fseek(fds[fd], off, whence == 1 ? SEEK_CUR : whence == 2 ? SEEK_END : SEEK_SET); + finish(4, (s32)ftell(fds[fd])); + return 0; +} + +/* 0x0012E050: sceMcRead(fd, buf, size): result is the byte count */ +s32 uyafn_12E050(s32 fd, void *buf, s32 size) +{ + if (fd < 0 || fd >= 8 || !fds[fd]) { finish(5, -4); return 0; } + finish(5, (s32)fread(buf, 1, (size_t)size, fds[fd])); + return 0; +} + +/* 0x0012E168: sceMcWrite(fd, buf, size): result is the byte count */ +s32 uyafn_12E168(s32 fd, void *buf, s32 size) +{ + if (fd < 0 || fd >= 8 || !fds[fd]) { finish(6, -4); return 0; } + s32 n = (s32)fwrite(buf, 1, (size_t)size, fds[fd]); + fflush(fds[fd]); + finish(6, n); + return 0; +} + +/* 0x0012E7A8: sceMcDelete(port, slot, name): a file, or an empty folder */ +s32 uyafn_12E7A8(s32 port, s32 slot, char *name) +{ + (void)slot; + char p[1024]; + if (!card_present(port)) { finish(0xF, -10); return 0; } + if (!host_path(port, name, p, sizeof p, 0, 0)) { finish(0xF, -4); return 0; } + if (is_dir(p)) { + DIR *d = opendir(p); struct dirent *e; int empty = 1; + while (d && (e = readdir(d))) if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) empty = 0; + if (d) closedir(d); + if (!empty) { finish(0xF, -6); return 0; } + finish(0xF, rmdir_one(p) == 0 ? 0 : -5); + return 0; + } + finish(0xF, remove(p) == 0 ? 0 : -4); + return 0; +} + +/* 0x0012E6D8: sceMcFormat(port, slot). The folder card is always formatted. */ +s32 uyafn_12E6D8(s32 port, s32 slot) { (void)slot; finish(0x10, card_present(port) ? 0 : -10); return 0; } + +/* 0x0012E8D8: sceMcUnformat(port, slot). Not done to a folder: reports success. */ +s32 uyafn_12E8D8(s32 port, s32 slot) { (void)slot; finish(0x11, card_present(port) ? 0 : -10); return 0; } + +/* Trace of the menu's card manager (uyafn_397490, state at 0x00142430) and + * of the card dialogs (UI state 0x001D4CE8, flags 0x001D4CEC). Called every + * vsync; logs a line whenever one of the watched values changes. The manager's + * idle polling (states 0 to 2) is left out. */ +void uya_mc_trace(void) +{ + if (!mclog()) return; + const u8 *m = (const u8 *)0x00142430; + s32 v[9]; + s32 st = *(const s32 *)(m + 0x15C); + v[0] = st > 2 ? st : 0; /* job running (3 and up) */ + v[1] = *(const s32 *)(m + 0x164); /* job requested, -1 none */ + v[2] = *(const s32 *)(m + 0x16C); /* last error/event code */ + v[3] = *(const s32 *)(m + 0x10); /* slot 1 "card changed" flag */ + v[4] = *(const s16 *)(m + 0x18); /* slot 1 save state */ + v[5] = *(const s32 *)(m + 0x160); /* job step */ + v[6] = *(const s32 *)0x001D4CE8; /* card dialog state */ + v[7] = *(const s32 *)0x001D4CEC; /* card dialog flags */ + v[8] = *(const s32 *)(m + 0xA0 + 0x10); /* slot 2 "card changed" flag */ + static s32 prev[9]; static int init; + if (init && !memcmp(prev, v, sizeof v)) return; + init = 1; memcpy(prev, v, sizeof v); + uya_log("card mgr: job %d step %d req %d err 0x%X | slot1 changed %d save %d | slot2 changed %d | dialog %d flags 0x%X", + v[0], v[5], v[1], v[2], v[3], v[4], v[8], v[6], v[7]); +} diff --git a/games/rac1/host/src/sdk/pad.c b/games/rac1/host/src/sdk/pad.c new file mode 100644 index 0000000..7f4eb77 --- /dev/null +++ b/games/rac1/host/src/sdk/pad.c @@ -0,0 +1,40 @@ +/* libpad / libmtap: one plain digital controller on port 0, driven by the + * keyboard (see plat_pad_buttons). Reporting a digital pad skips the DualShock + * actuator setup in the menu's pad state machine (uyafn_39E4B0). */ +#include "uya_host.h" +unsigned plat_pad_buttons(void); /* keyboard (Windows) or UYA_KEYS script (Linux), 1 = held */ + +/* 0x0012F470: scePadGetState(port, slot); 6 = stable (ready). */ +s32 uyafn_12F470(s32 port, s32 slot) { return (port == 0 && slot == 0) ? 6 : 0; } + +/* 0x0012F728: scePadInfoMode(port, slot, term, offs); term 1 = current ID (4 = digital), 2 = extended ID (0 = none). */ +s32 uyafn_12F728(s32 port, s32 slot, s32 term, s32 offs) +{ + (void)port; (void)slot; (void)offs; + return term == 1 ? 4 : 0; +} + +/* 0x0012EBB8: sceMtapGetConnection(port); 0 = no multitap. */ +s32 uyafn_12EBB8(s32 port) { (void)port; return 0; } + +/* 0x0012F3F8: scePadRead(port, slot, buf): 32 bytes; [0] 0 = ok, [1] 0x41 = digital, [2..3] buttons (1 = released). */ +s32 uyafn_12F3F8(s32 port, s32 slot, u8 *buf) +{ + for (int i = 0; i < 32; i++) buf[i] = 0; + if (port || slot) return 0; + buf[1] = 0x41; + { + unsigned held = plat_pad_buttons(); + buf[2] = (u8)~(held & 0xFF); /* Select L3 R3 Start Up Right Down Left */ + buf[3] = (u8)~((held >> 8) & 0xFF); /* L2 R2 L1 R1 Triangle Circle Cross Square */ + } + for (int i = 4; i < 8; i++) buf[i] = 0x80; /* sticks centred, if anyone looks */ + return 32; +} + +/* 0x0012F5A8: scePadGetReqState(port, slot); 0 = complete. */ +s32 uyafn_12F5A8(s32 port, s32 slot) { (void)port; (void)slot; return 0; } + +/* 0x0012F918: scePadSetActAlign(port, slot, data): maps the pad's vibration + * motors. There is no vibration on the keyboard, so it reports success. */ +s32 uyafn_12F918(s32 port, s32 slot, u8 *data) { (void)port; (void)slot; (void)data; return 1; } diff --git a/games/rac1/host/src/sdk/sound.c b/games/rac1/host/src/sdk/sound.c new file mode 100644 index 0000000..a53ae9d --- /dev/null +++ b/games/rac1/host/src/sdk/sound.c @@ -0,0 +1,353 @@ +/* 989snd (Sony's sound library, EE side) for the menu's sound effects, with a + * small software mixer in place of the SPU2. + * + * Banks are 989snd "SBlk" version 3 banks (sounds -> grains -> tones -> PS2 + * ADPCM waveforms). Loading copies the bank into host memory. Playing a sound + * starts one voice per "play tone" grain of the sound; voices are decoded and + * mixed at 48 kHz stereo and streamed to the platform's audio output each + * vsync (about 100 ms ahead). Volume (0-0x400), pan (degrees) and pitch (1/128 semitone) follow the + * library's units. Completion callbacks are queued and delivered on the next + * flush (uyafn_13B620), like the asynchronous library. + * + * The SPU2's ADSR envelopes are applied; a stop is a key-off (release). + * Not done: reverb, the grain types other than "play tone" (random choice, + * delays...), music streams (uyafn_13CB20) and master volume groups. + * The menu also calls these every frame and while it waits for disc reads, so + * they also deliver finished disc reads. */ +#include +#include +#include +#include +#include "uya_host.h" +#include "uya_runtime.h" + +void uya_cd_service(void); + +/* ---- banks ---- */ +#define MAX_BANKS 16 +typedef struct { u8 *data; u32 size; const u8 *sblk; const u8 *wave; u32 wave_size; } bank_t; +static bank_t banks[MAX_BANKS]; /* handle = index + 1 */ + +static u32 rd32(const u8 *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((u32)p[3] << 24); } +static u16 rd16(const u8 *p) { return (u16)(p[0] | (p[1] << 8)); } + +static s32 bank_add(const u8 *src, u32 size) +{ + if (size < 0x18 || rd32(src + 8) + 4 > size || memcmp(src + rd32(src + 8), "SBlk", 4)) { + uya_log("sound: not a 989snd bank"); + return 0; + } + for (int i = 0; i < MAX_BANKS; i++) { + if (banks[i].data) continue; + u8 *d = malloc(size); + if (!d) return 0; + memcpy(d, src, size); + banks[i].data = d; + banks[i].size = size; + banks[i].sblk = d + rd32(d + 8); + banks[i].wave = d + rd32(d + 0x10); + banks[i].wave_size = rd32(d + 0x14); + uya_log("sound: bank %d loaded (%u sounds, %u KB of samples)", i + 1, rd16(banks[i].sblk + 0x16), banks[i].wave_size / 1024); + return i + 1; + } + return 0; +} + +static u32 bank_size(const u8 *b) { return rd32(b + 0x10) + rd32(b + 0x14); } + +/* ---- callbacks, delivered on the next flush ---- */ +typedef void (*snd_cb)(s32 handle, u64 user); +static struct { snd_cb cb; s32 h; u64 user; } cbq[256]; +static int ncb; + +static void queue_cb(void *cb, s32 h, u64 user) +{ + if (!cb || ncb >= 256) return; + cbq[ncb].cb = (snd_cb)cb; cbq[ncb].h = h; cbq[ncb].user = user; ncb++; +} + +static void run_cbs(void) +{ + int n = ncb; + ncb = 0; + for (int i = 0; i < n; i++) cbq[i].cb(cbq[i].h, cbq[i].user); +} + +/* ---- voices ---- */ +#define MAX_VOICES 48 +typedef struct { + int on; + s32 handle; + const u8 *data, *end, *loop; + s32 hist1, hist2; + s16 buf[28]; + int bufpos; /* next sample in buf, 28 = decode next frame */ + double frac, step; /* resampling */ + s16 hist4[4]; /* last four samples for the interpolation */ + int env_phase; /* 0 attack, 1 decay, 2 sustain, 3 release */ + int env_level, env_count; + u16 adsr1, adsr2; + float gl, gr; + double base_rate; + int tone_pan; + float tone_vol; +} voice_t; +static voice_t voices[MAX_VOICES]; +static s32 next_handle = 1; + +static int decode_frame(voice_t *v) +{ + static const int fk[5][2] = {{0, 0}, {60, 0}, {115, -52}, {98, -55}, {122, -60}}; + if (v->data + 16 > v->end) return 0; + const u8 *fr = v->data; + int sh = fr[0] & 15, flt = (fr[0] >> 4) & 7, flags = fr[1]; + if (flt > 4) flt = 0; + if (sh > 12) sh = 9; + if (flags & 4) v->loop = fr; + for (int s = 0; s < 28; s++) { + int nib = (fr[2 + s / 2] >> ((s & 1) * 4)) & 15; + if (nib >= 8) nib -= 16; + int x = (nib << 12) >> sh; + x += (v->hist1 * fk[flt][0] + v->hist2 * fk[flt][1] + 32) >> 6; + if (x > 32767) x = 32767; + if (x < -32768) x = -32768; + v->hist2 = v->hist1; + v->hist1 = x; + v->buf[s] = (s16)x; + } + v->bufpos = 0; + v->data += 16; + if (flags & 1) { /* end of the waveform */ + if ((flags & 2) && v->loop) v->data = v->loop; + else v->end = v->data; /* play this frame, then stop */ + } + return 1; +} + +static int next_sample(voice_t *v, s16 *out) +{ + if (v->bufpos >= 28 && !decode_frame(v)) return 0; + *out = v->buf[v->bufpos++]; + return 1; +} + +static void set_mix(voice_t *v, s32 vol, s32 pan, s32 pitch) +{ + int deg = ((pan + v->tone_pan) % 360 + 360) % 360; + double x = sin((double)deg * 3.14159265358979 / 180.0); /* -1 left .. 1 right */ + float g = (float)(vol > 0x400 ? 0x400 : (vol < 0 ? 0 : vol)) / 1024.0f * v->tone_vol; + v->gl = g * (float)sqrt((1.0 - x) * 0.5); + v->gr = g * (float)sqrt((1.0 + x) * 0.5); + v->step = v->base_rate * pow(2.0, (double)pitch / 1536.0) / 48000.0; +} + +/* rate of a tone: centre note (0x80 and up: 48 kHz base) and fine tune, for note 60 */ +static double tone_rate(u8 center, u8 fine) +{ + double base = 44100.0; + int c = center; + if (center & 0x80) { c = 0x100 - center; base = 48000.0; } + return base * pow(2.0, ((60 - c) + fine / 128.0) / 12.0); +} + +static s32 play_sound(s32 bank, s32 sound, s32 vol, s32 pan, s32 pitch) +{ + if (bank < 1 || bank > MAX_BANKS || !banks[bank - 1].data) return 0; + bank_t *b = &banks[bank - 1]; + const u8 *s = b->sblk; + u32 nsounds = rd16(s + 0x16); + if (sound < 0 || (u32)sound >= nsounds) return 0; + const u8 *t1 = s + rd32(s + 0x1C), *t2 = s + rd32(s + 0x20), *t3 = s + rd32(s + 0x34); + const u8 *se = t1 + sound * 12; + int ngrains = se[4]; + u32 goff = rd32(se + 8); + float svol = se[0] / 127.0f; + s32 h = 0; + for (int g = 0; g < ngrains; g++) { + const u8 *gr = t2 + goff + g * 8; + u32 val = rd32(gr); + if ((val >> 16) != 0x0100) continue; /* only "play tone" grains */ + const u8 *tone = t3 + (val & 0xFFFF); + u32 off = rd32(tone + 0x10), size = rd32(tone + 0x14); + if (off >= b->wave_size) continue; + if (off + size > b->wave_size || size == 0) size = b->wave_size - off; + int vi; + for (vi = 0; vi < MAX_VOICES && voices[vi].on; vi++) {} + if (vi == MAX_VOICES) break; + if (!h) { h = next_handle++; if (next_handle > 0x7FFFFFFF) next_handle = 1; } + voice_t *v = &voices[vi]; + memset(v, 0, sizeof *v); + v->on = 1; + v->handle = h; + v->data = b->wave + off; + v->end = b->wave + off + size; + v->bufpos = 28; + v->frac = 4.0; /* load the first samples */ + v->adsr1 = rd16(tone + 0xA); + v->adsr2 = rd16(tone + 0xC); + v->base_rate = tone_rate(tone[2], tone[3]); + v->tone_vol = svol * ((tone[1] > 127 ? 127 : tone[1]) / 127.0f); + v->tone_pan = (s16)rd16(tone + 4); + set_mix(v, vol, pan, pitch); + if (getenv("UYA_SNDLOG")) uya_log("sound: play bank %d sound %d tone at 0x%X, %.0f Hz, vol %d pan %d pitch %d", bank, sound, off, v->base_rate, vol, pan, pitch); + } + return h; +} + +static int handle_playing(s32 h) +{ + if (!h || h == -1) return 0; + for (int i = 0; i < MAX_VOICES; i++) if (voices[i].on && voices[i].handle == h) return 1; + return 0; +} + +/* The SPU2's ADSR envelope, one step per 48 kHz sample (rates as on the SPU). */ +static void env_step(voice_t *v) +{ + int rate, inc, expo; + switch (v->env_phase) { + case 0: rate = (v->adsr1 >> 8) & 0x7F; inc = 1; expo = v->adsr1 >> 15; break; + case 1: rate = ((v->adsr1 >> 4) & 0xF) << 2; inc = 0; expo = 1; break; + case 2: rate = (v->adsr2 >> 6) & 0x7F; inc = !((v->adsr2 >> 14) & 1); expo = v->adsr2 >> 15; break; + default: rate = (v->adsr2 & 0x1F) << 2; inc = 0; expo = (v->adsr2 >> 5) & 1; break; + } + if (v->env_phase == 2 && rate == 0x7F) return; /* sustain held */ + int shift = rate >> 2; + int step = inc ? 7 - (rate & 3) : -8 + (rate & 3); + int cycles = 1 << (shift > 11 ? shift - 11 : 0); + step <<= (shift < 11 ? 11 - shift : 0); + if (expo && inc && v->env_level > 0x6000) cycles *= 4; + if (expo && !inc) step = step * v->env_level >> 15; + if (++v->env_count < cycles) return; + v->env_count = 0; + v->env_level += step; + if (v->env_level > 0x7FFF) v->env_level = 0x7FFF; + if (v->env_level < 0) v->env_level = 0; + if (v->env_phase == 0 && v->env_level >= 0x7FFF) v->env_phase = 1; + else if (v->env_phase == 1 && v->env_level <= (((v->adsr1 & 0xF) + 1) * 0x800)) v->env_phase = 2; + else if (v->env_phase == 3 && v->env_level == 0) v->on = 0; +} + +/* Mixes `frames` stereo frames (48 kHz). Resampling is 4-point cubic + * B-spline, a smoothing filter like the SPU's Gaussian interpolation. */ +static void mix(s16 *out, int frames) +{ + for (int f = 0; f < frames; f++) { + float l = 0, r = 0; + for (int i = 0; i < MAX_VOICES; i++) { + voice_t *v = &voices[i]; + if (!v->on) continue; + while (v->frac >= 1.0) { + s16 x; + if (!next_sample(v, &x)) { v->on = 0; break; } + v->hist4[0] = v->hist4[1]; v->hist4[1] = v->hist4[2]; v->hist4[2] = v->hist4[3]; v->hist4[3] = x; + v->frac -= 1.0; + } + if (!v->on) continue; + float t = (float)v->frac, t2 = t * t, t3 = t2 * t; + float w0 = (1 - t) * (1 - t) * (1 - t) / 6.0f; + float w1 = (3 * t3 - 6 * t2 + 4) / 6.0f; + float w2 = (-3 * t3 + 3 * t2 + 3 * t + 1) / 6.0f; + float w3 = t3 / 6.0f; + float s = v->hist4[0] * w0 + v->hist4[1] * w1 + v->hist4[2] * w2 + v->hist4[3] * w3; + env_step(v); + s *= v->env_level / 32768.0f; + l += s * v->gl; + r += s * v->gr; + v->frac += v->step; + } + out[f * 2] = (s16)(l > 32767 ? 32767 : (l < -32768 ? -32768 : l)); + out[f * 2 + 1] = (s16)(r > 32767 ? 32767 : (r < -32768 ? -32768 : r)); + } +} + +/* RAC1: uya_sound_pump (called once per vsync) is in host/src/rac1/snd989.c, + * which mixes the host IOP driver's voices; the UYA mixer above is unused. */ + +/* ---- the library calls ---- */ + +/* 0x0013B620: flush queued commands; 0 = nothing pending. Delivers callbacks. */ +s32 uyafn_13B620(void) { uya_cd_service(); run_cbs(); return 0; } +/* 0x0013CA28, 0x0013CA20: per-frame library service. */ +void uyafn_13CA28(void) { uya_cd_service(); } +void uyafn_13CA20(void) { uya_cd_service(); } + +/* 0x0013BC30: load a bank from EE memory; cb(handle, user) when done. */ +s32 uyafn_13BC30(u32 addr, void *cb, u64 user) +{ + const u8 *src = (const u8 *)(uintptr_t)addr; + s32 h = bank_add(src, bank_size(src)); + queue_cb(cb, h, user); + return h; +} + +/* 0x0013BAB8: load a bank from the disc at sector lsn; cb(handle, user) when done. */ +s32 uyafn_13BAB8(s32 lsn, s32 offset, void *cb, u64 user) +{ + u8 head[2048]; + s32 h = 0; + (void)offset; + if (uya_disc_read((u32)lsn, 1, head) == 0) { + u32 size = bank_size(head); + u32 sectors = (size + 2047) / 2048; + u8 *b = (size > 0x18 && size < (16u << 20)) ? malloc((size_t)sectors * 2048) : 0; + if (b && uya_disc_read((u32)lsn, sectors, b) == 0) h = bank_add(b, size); + free(b); + } + queue_cb(cb, h, user); + return h; +} + +/* 0x0013C0B8: play `sound` from `bank` with volume, pan, pitch mod and pitch bend. */ +s32 uyafn_13C0B8(s32 bank, s32 sound, s32 vol, s32 pan, s32 pitch, s32 pb, void *cb, u32 user) +{ + (void)pb; + s32 h = play_sound(bank, sound, vol, pan, pitch); + queue_cb(cb, h, user); + return h; +} + +/* 0x0013C2C0: update a playing sound (mode bits: 1 volume, 2 pan, 8 pitch, 0x10 bend). */ +s32 uyafn_13C2C0(s32 h, s32 mode, s32 vol, s32 pan, s32 pitch, s32 pb, void *cb, u32 user) +{ + (void)mode; (void)pb; + for (int i = 0; i < MAX_VOICES; i++) + if (voices[i].on && voices[i].handle == h) set_mix(&voices[i], vol, pan, pitch); + queue_cb(cb, handle_playing(h) ? h : 0, user); + return 0; +} + +/* 0x0013C200: is the sound still playing? cb(handle or 0, user). */ +s32 uyafn_13C200(s32 h, void *cb, u32 user) +{ + queue_cb(cb, handle_playing(h) ? h : 0, user); + return 0; +} + +/* 0x0013C118: stop a sound. */ +void uyafn_13C118(s32 h) +{ + for (int i = 0; i < MAX_VOICES; i++) + if (voices[i].on && voices[i].handle == h) { voices[i].env_phase = 3; voices[i].env_count = 0; } /* key off: release */ +} + +/* 0x0013C170: stop every sound. */ +void uyafn_13C170(void) { for (int i = 0; i < MAX_VOICES; i++) voices[i].on = 0; } +/* 0x0013BF00, 0x0013BED0: unload a bank (kept: the handle is not reused). */ +void uyafn_13BF00(s32 h) { (void)h; } +void uyafn_13BED0(s32 h) { (void)h; } + +/* 0x0013BF30: master volume of a group (not applied yet). */ +void uyafn_13BF30(s32 group, s32 vol) { (void)group; (void)vol; } +/* 0x0013CF80, 0x0013CFF8: reverb set-up (not emulated). */ +void uyafn_13CF80(s32 a, s32 b, s32 c, s32 d, s32 e) { (void)a; (void)b; (void)c; (void)d; (void)e; } +void uyafn_13CFF8(s32 a, s32 b, s32 c, s32 d) { (void)a; (void)b; (void)c; (void)d; } +/* 0x0013D1E8: distance to pitch shift (Doppler); none. */ +s32 uyafn_13D1E8(s32 d) { (void)d; return 0; } + +/* 0x0013CAE8 and 0x0013CD00(n): 989snd commands 0x34 and 0x36, which the menu + * sends after stopping every sound when a movie starts (uyafn_39CBA0). They + * act on the music stream player, which the host does not have yet, so both + * succeed and do nothing. */ +s32 uyafn_13CAE8(void) { return 0; } +s32 uyafn_13CD00(s32 n) { (void)n; return 0; } diff --git a/games/rac1/launcher/launcher_win32.c b/games/rac1/launcher/launcher_win32.c new file mode 100644 index 0000000..0d10bfa --- /dev/null +++ b/games/rac1/launcher/launcher_win32.c @@ -0,0 +1,104 @@ +/* rac1_wrapper.exe: reserves PS2 RAM through its own image, then runs rac1_game.dll. + * + * Built with no C runtime and image base 0x10000. Its .ps2ram section (see + * ps2ram_win32.s) is uninitialized and runs past 0x02000000, so Windows maps it + * with the image, before it places the main stack and the process heaps. + * The few Win32 functions it needs are declared here, so it needs no SDK headers. + * + * Delivery in parts: with every level built in, rac1_game.dll is larger than the + * file transfer allows, so it can arrive as rac1_game.dll.part00, .part01, ... plus + * rac1_game.dll.manifest (" "). On start, if the manifest is + * there, the parts are joined into rac1_game.dll (through a temporary file, the size + * checked against the manifest) and the parts and manifest are deleted. + * No globals here: they would land in .bss next to the PS2 RAM. */ +typedef void *HMODULE; +typedef void *HANDLE; +__declspec(dllimport) HMODULE __stdcall LoadLibraryA(const char *); +__declspec(dllimport) void *__stdcall GetProcAddress(HMODULE, const char *); +__declspec(dllimport) void __stdcall ExitProcess(unsigned); +__declspec(dllimport) int __stdcall MessageBoxA(void *, const char *, const char *, unsigned); +__declspec(dllimport) HANDLE __stdcall CreateFileA(const char *, unsigned, unsigned, void *, unsigned, unsigned, HANDLE); +__declspec(dllimport) int __stdcall ReadFile(HANDLE, void *, unsigned, unsigned *, void *); +__declspec(dllimport) int __stdcall WriteFile(HANDLE, const void *, unsigned, unsigned *, void *); +__declspec(dllimport) int __stdcall CloseHandle(HANDLE); +__declspec(dllimport) int __stdcall DeleteFileA(const char *); +__declspec(dllimport) int __stdcall MoveFileExA(const char *, const char *, unsigned); +__declspec(dllimport) void *__stdcall VirtualAlloc(void *, unsigned long, unsigned, unsigned); +__declspec(dllimport) int __stdcall VirtualFree(void *, unsigned long, unsigned); +__declspec(dllimport) unsigned __stdcall GetModuleFileNameA(HMODULE, char *, unsigned); + +#define INVALID ((HANDLE)(long)-1) +#define GENERIC_READ 0x80000000u +#define GENERIC_WRITE 0x40000000u +#define OPEN_EXISTING 3u +#define CREATE_ALWAYS 2u + +typedef int (*uya_main_fn)(void); + +/* dir (the exe's folder, with its trailing backslash) + name, plus a two-digit number when i >= 0 */ +static void path(char *out, const char *dir, const char *name, int i) +{ + int k = 0; + while (*dir) out[k++] = *dir++; + while (*name) out[k++] = *name++; + if (i >= 0) { out[k++] = (char)('0' + i / 10); out[k++] = (char)('0' + i % 10); } + out[k] = 0; +} + +static void fail(const char *m) { MessageBoxA(0, m, "RAC1", 0x10); ExitProcess(1); } + +/* rac1_game.dll from its parts, when a manifest says they are there */ +static void join_parts(void) +{ + char dir[600], man[700], tmp[700], dll[700], nm[700]; + unsigned dl = GetModuleFileNameA(0, dir, 520); + while (dl > 0 && dir[dl - 1] != '\\' && dir[dl - 1] != '/') dl--; + dir[dl] = 0; + path(man, dir, "rac1_game.dll.manifest", -1); path(tmp, dir, "rac1_game.dll.tmp", -1); path(dll, dir, "rac1_game.dll", -1); + HANDLE m = CreateFileA(man, GENERIC_READ, 1, 0, OPEN_EXISTING, 0x80, 0); + if (m == INVALID) return; + char txt[64]; unsigned got = 0; + ReadFile(m, txt, sizeof txt - 1, &got, 0); + CloseHandle(m); + txt[got] = 0; + unsigned parts = 0, k = 0; unsigned long long total = 0; + while (txt[k] >= '0' && txt[k] <= '9') parts = parts * 10 + (unsigned)(txt[k++] - '0'); + while (txt[k] == ' ') k++; + while (txt[k] >= '0' && txt[k] <= '9') total = total * 10 + (unsigned)(txt[k++] - '0'); + if (!parts || parts > 99 || !total) fail("rac1_game.dll.manifest is not valid."); + unsigned char *buf = VirtualAlloc(0, 1u << 22, 0x3000, 4); + if (!buf) fail("Out of memory joining rac1_game.dll."); + HANDLE o = CreateFileA(tmp, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0x80, 0); + if (o == INVALID) fail("Could not write rac1_game.dll.tmp next to rac1_wrapper.exe."); + unsigned long long done = 0; + for (unsigned i = 0; i < parts; i++) { + path(nm, dir, "rac1_game.dll.part", (int)i); + HANDLE p = CreateFileA(nm, GENERIC_READ, 1, 0, OPEN_EXISTING, 0x80, 0); + if (p == INVALID) { CloseHandle(o); DeleteFileA(tmp); fail("A part of rac1_game.dll (rac1_game.dll.partNN) is missing."); } + for (;;) { + unsigned n = 0, w = 0; + if (!ReadFile(p, buf, 1u << 22, &n, 0) || !n) break; + if (!WriteFile(o, buf, n, &w, 0) || w != n) { CloseHandle(p); CloseHandle(o); fail("Writing rac1_game.dll.tmp failed (disk full?)."); } + done += n; + } + CloseHandle(p); + } + CloseHandle(o); + VirtualFree(buf, 0, 0x8000); + if (done != total) { DeleteFileA(tmp); fail("The parts of rac1_game.dll do not add up to its size; copy them again."); } + if (!MoveFileExA(tmp, dll, 1 /* REPLACE_EXISTING */)) fail("Could not replace rac1_game.dll (is the game still running?)."); + for (unsigned i = 0; i < parts; i++) { path(nm, dir, "rac1_game.dll.part", (int)i); DeleteFileA(nm); } + DeleteFileA(man); +} + +void __stdcall launcher_start(void) +{ + join_parts(); + HMODULE h = LoadLibraryA("rac1_game.dll"); + if (!h) { + MessageBoxA(0, "rac1_game.dll was not found next to rac1_wrapper.exe.", "UYA Wrapper", 0x10); + ExitProcess(1); + } + uya_main_fn f = (uya_main_fn)GetProcAddress(h, "uya_main_win32"); + ExitProcess(f ? (unsigned)f() : 1u); +} diff --git a/games/rac1/launcher/ps2ram_win32.c b/games/rac1/launcher/ps2ram_win32.c new file mode 100644 index 0000000..81147c0 --- /dev/null +++ b/games/rac1/launcher/ps2ram_win32.c @@ -0,0 +1,4 @@ +/* PS2 main RAM, part of the launcher image (see launcher_win32.c). A plain + * zero-initialized global lands in .bss, so it costs no file space; the + * runtime makes it executable. */ +__attribute__((aligned(4096))) unsigned char uya_ps2ram[0x01FF8000]; diff --git a/games/rac1/tools/gen_glthread.py b/games/rac1/tools/gen_glthread.py new file mode 100644 index 0000000..788044e --- /dev/null +++ b/games/rac1/tools/gen_glthread.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""gen_glthread.py: writes host/src/gs/gs_gl_thread.h from the GL function list +in host/src/gs/gs_gl_api.h (GSGL_FUNCS). + +The GL renderer (gs_gl.c) calls GL through p_glX. With the GL thread on, each +p_glX records the call (its arguments, and copies of the data it points at) in +a queue that the GL thread, which owns the context, replays through r_glX (the +real entry points). Calls that return something or write through a pointer +(glGet*, glGen*, glCreate*, glReadPixels, glMapBufferRange...) are run by the +GL thread while the caller waits. Without the GL thread p_glX calls r_glX. + +Run it again after changing GSGL_FUNCS: python3 tools/gen_glthread.py +""" +import os, re + +HERE = os.path.dirname(os.path.abspath(__file__)) +API = os.path.join(HERE, "..", "host", "src", "gs", "gs_gl_api.h") +OUT = os.path.join(HERE, "..", "host", "src", "gs", "gs_gl_thread.h") + +src = open(API).read() +funcs = re.findall(r"X\((gl\w+),\s*([^,]+?),\s*\(([^)]*)\)\)", src) + +SCALAR = {"GLenum", "GLuint", "GLbitfield", "GLint", "GLsizei", "GLboolean", "GLubyte", "GLsizeiptr", "GLintptr"} +FLOAT = {"GLfloat"} +# calls whose pointer argument is data to copy into the queue (bytes: a C expression) +DATA = { + "glTexImage2D": (8, "glt_pix_bytes(a3, a4, a6, a7)"), + "glTexSubImage2D": (8, "glt_pix_bytes(a4, a5, a6, a7)"), + "glBufferData": (2, "(size_t)a1"), + "glBufferSubData": (3, "(size_t)a2"), + "glUniform4iv": (2, "(size_t)a1 * 16"), + "glUniform1iv": (2, "(size_t)a1 * 4"), + "glDeleteTextures": (1, "(size_t)a0 * 4"), + "glDeleteFramebuffers": (1, "(size_t)a0 * 4"), +} +# pointer arguments that are offsets into the bound buffer, not data +OFFSET_PTR = {"glVertexAttribPointer": 5, "glVertexAttribIPointer": 4} + +out = ['/* generated by tools/gen_glthread.py from gs_gl_api.h: do not edit */', + '/* included by gs_gl.c, after the GL thread helpers (glt_cmd, glt_call, glt_q, glt_pix_bytes) */'] +ids = ["GLT_" + f[0] for f in funcs] +out.append("enum { " + ", ".join(ids) + ", GLT_SWAP, GLT_VB, GLT_CALLBACK, GLT_N };") +out.append("#define GSGL_DEF(name, ret, args) PFN_##name r_##name;\nGSGL_FUNCS(GSGL_DEF)\n#undef GSGL_DEF") + +def conv_in(t, name): + """an argument to its u64 slot""" + t = t.strip() + if t in FLOAT: return "glt_fbits(%s)" % name + if "*" in t: return "(u64)(uintptr_t)(%s)" % name + return "(u64)(s64)(%s)" % name + +def conv_out(t, slot): + """a u64 slot back to the argument's type""" + t = t.strip() + if t in FLOAT: return "glt_bitsf(%s)" % slot + if "*" in t: return "(%s)(uintptr_t)(%s)" % (t, slot) + return "(%s)(%s)" % (t, slot) + +replay = [] +for name, ret, args in funcs: + ret = ret.strip() + ts = [] if args.strip() in ("void", "") else [a.strip() for a in args.split(",")] + n = len(ts) + params = ", ".join("%s a%d" % (t, i) for i, t in enumerate(ts)) or "void" + call_r = "r_%s(%s)" % (name, ", ".join("a%d" % i for i in range(n))) + ptr_args = [i for i, t in enumerate(ts) if "*" in t] + data = DATA.get(name) + is_async = ret == "void" and (not ptr_args or data or (name in OFFSET_PTR and ptr_args == [OFFSET_PTR[name]])) + body = [] + if name == "glPixelStorei": body.append(" glt_track_store(a0, a1); /* the unpack layout the queued texel copies are sized by */") + if is_async: + body.append(" if (!glt_q) { %s; return; }" % call_r) + if data: + pi, expr = data + body.append(" size_t nb = a%d ? %s : 0;" % (pi, expr)) + body.append(" u64 *c = glt_cmd(%s, %d, nb);" % ("GLT_" + name, n)) + else: + body.append(" u64 *c = glt_cmd(%s, %d, 0);" % ("GLT_" + name, n)) + for i, t in enumerate(ts): + if data and i == data[0]: + body.append(" c[%d] = 0; if (nb) memcpy(c + %d, a%d, nb);" % (i, n, i)) + else: + body.append(" c[%d] = %s;" % (i, conv_in(t, "a%d" % i))) + # replay: the data pointer is the blob after the arguments + rargs = [] + for i, t in enumerate(ts): + if data and i == data[0]: rargs.append("blob_n ? (%s)(const void *)(c + %d) : (%s)0" % (t, n, t)) + else: rargs.append(conv_out(t, "c[%d]" % i)) + replay.append(" case GLT_%s: r_%s(%s); break;" % (name, name, ", ".join(rargs))) + else: + if ret == "void": body.append(" if (!glt_q) { %s; return; }" % call_r) + else: body.append(" if (!glt_q) return %s;" % call_r) + body.append(" u64 a[%d + 1]; u64 r = 0;" % n) + for i, t in enumerate(ts): body.append(" a[%d] = %s;" % (i, conv_in(t, "a%d" % i))) + body.append(" glt_call(%s, a, %d, &r);" % ("GLT_" + name, n)) + if ret != "void": + if ret in FLOAT: body.append(" return glt_bitsf(r);") + elif "*" in ret: body.append(" return (%s)(uintptr_t)r;" % ret) + else: body.append(" return (%s)r;" % ret) + rargs = ", ".join(conv_out(t, "c[%d]" % i) for i, t in enumerate(ts)) + if ret == "void": replay.append(" case GLT_%s: r_%s(%s); break;" % (name, name, rargs)) + elif "*" in ret: replay.append(" case GLT_%s: *rp = (u64)(uintptr_t)r_%s(%s); break;" % (name, name, rargs)) + else: replay.append(" case GLT_%s: *rp = (u64)r_%s(%s); break;" % (name, name, rargs)) + out.append("static %s p_%s(%s)\n{\n%s\n}" % (ret, name, params, "\n".join(body))) + +nargs = [str(0 if a.strip() in ("void", "") else len(a.split(","))) for _, _, a in funcs] +out.append("static const unsigned char glt_nargs[GLT_N] = { " + ", ".join(nargs) + ", 0, 1, 0 }; /* ..., SWAP, VB, CALLBACK */") +out.append("/* the GL thread: one recorded call (c: its arguments, then its data; rp: where a result goes) */") +out.append("static void glt_exec(u32 id, const u64 *c, size_t blob_n, u64 *rp)\n{\n (void)blob_n; (void)rp;\n switch (id) {") +out += replay +out.append(" default: glt_exec_extra(id, c, blob_n); break;\n }\n}") +open(OUT, "w").write("\n".join(out) + "\n") +print("wrote %s: %d GL functions" % (OUT, len(funcs))) diff --git a/games/rac1/tools/gen_host.py b/games/rac1/tools/gen_host.py new file mode 100644 index 0000000..897cf6e --- /dev/null +++ b/games/rac1/tools/gen_host.py @@ -0,0 +1,1264 @@ +#!/usr/bin/env python3 +"""gen_host.py DECOMP_REPO OUT_DIR + +RAC1 Wrapper: turns OpenRAC rac1-decomp's executable game code (src/game, +src/game/movie and the game-compiled parts of src/core) into sources a 32-bit +x86 compiler accepts. Adapted from the UYA Wrapper's generator. +The decomp repo is only read, never written. + +Per file: + - INCLUDE_ASM / ASM_FUNC entries become stubs in asm_stubs.c (they log and + return 0) unless host/overrides/.c provides a real version; + LINKER_REMNANT, INCLUDE_RODATA, TEXT_PADDING and top-level __asm__ go away. + - A function whose body uses inline asm (VU0, COP0, sync...), or calls a + same-file static helper that does, gets a stub body, again unless an + override exists. + - `long` becomes `long long` (the EE's long is 64-bit), and + `typedef int X __attribute__((mode(TI)))` becomes a 16-byte struct. +Also writes: + - abs_syms.s: every symbol the decomp resolves by address and the host does + not define (globals, engine functions, per-function aliases), as absolute + symbols, so data lives at its PS2 address. + - funcs.tsv: every frontbin function address and how the host provides it. + +gen_host.py DECOMP_REPO OUT_DIR --level NN --exe-gen EXE_GEN_DIR + +Level mode: the same passes over one level's program (docs/LEVELS.md): the +shared code and the level's own code in src/overlays, keeping only the +functions the level's program has (config/overlays/functions.tsv). Its +functions sit at the level's addresses, so the executable's main-segment +names (func_XXXXXXXX of 0x1E9080-0x23E740) mean that function's copy in the +level. Every name the level pass defines or places is given the suffix _lNN +by one header, lNN_names.h, forced into each of its C files (asm labels are +rewritten to match), so several levels and the executable link together. +Writes OUT_DIR/{*.c, asm_stubs.c, missing.c, tramp_table.c, abs_syms.s, +sigs.json, asm_entries.json, lNN_names.h, funcs.tsv}; the headers are the +executable pass's (EXE_GEN_DIR/include). +""" +import os, re, sys, glob, json + +REPO, OUT = sys.argv[1], sys.argv[2] +LEVEL, EXEGEN = None, None +_a = sys.argv[3:] +while _a: + _o = _a.pop(0) + if _o == "--level": LEVEL = int(_a.pop(0)) + elif _o == "--exe-gen": EXEGEN = _a.pop(0) + else: sys.exit("gen_host.py: unknown option " + _o) +LV = "%02d" % LEVEL if LEVEL is not None else None +SFX = "_l%s" % LV if LEVEL is not None else "" +HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PLAT = os.environ.get("RAC_PLATFORM", os.environ.get("UYA_PLATFORM", "linux")) # linux | windows (COFF i386 adds a leading _) +os.makedirs(OUT, exist_ok=True) +overrides = {os.path.basename(p)[:-2] for p in glob.glob(os.path.join(HERE, "host", "overrides", "func_*.c"))} +ASM_MACRO = re.compile(r"^\s*(INCLUDE_ASM|ASM_FUNC|LINKER_REMNANT|INCLUDE_RODATA|TEXT_PADDING)\s*\(([^()]*)\)\s*;?\s*(?:/\*.*?\*/|//.*)?\s*$", re.M) + + +def strip_comments_keep_len(s): + """Blank out comments and string/char literals (same length) for scanning.""" + out = list(s); i = 0; n = len(s) + while i < n: + c = s[i] + if s.startswith("/*", i): + j = s.find("*/", i + 2); j = n if j < 0 else j + 2 + for k in range(i, j): + if out[k] != "\n": out[k] = " " + i = j + elif s.startswith("//", i): + j = s.find("\n", i); j = n if j < 0 else j + for k in range(i, j): out[k] = " " + i = j + elif c in "\"'": + j = i + 1 + while j < n and s[j] != c: + j += 2 if s[j] == "\\" else 1 + for k in range(i + 1, min(j, n)): + if out[k] != "\n": out[k] = "_" + i = j + 1 + else: + i += 1 + return "".join(out) + + +host_defined = set() +STATIC = os.environ.get("UYA_STATIC", "1") != "0" # RAC1_STATICRECOMP: translate the whole game from its bytes +for p in glob.glob(os.path.join(HERE, "host", "src", "**", "*.c"), recursive=True): + host_defined |= set(re.findall(r"^[\w \t\*]*\b(func_(?:L\d\d_)?[0-9A-Fa-f]{6,8})\s*\([^;{]*\)\s*\{", strip_comments_keep_len(open(p).read()), re.M)) + + +def match_close(m, i, o="{", c="}"): + d = 0 + for j in range(i, len(m)): + if m[j] == o: d += 1 + elif m[j] == c: + d -= 1 + if d == 0: return j + raise ValueError("unbalanced") + + +ASM_RE = re.compile(r'\b(__asm__|__asm|asm)\b(?!\s*__volatile__\s*\(\s*""\s*:::\s*"_+"\s*\)\s*;)') +IDENT = re.compile(r"[A-Za-z_]\w*") + + +def top_level_items(src): + """Yield (kind, start, end, extra) for top-level __asm__ statements and function definitions.""" + m = strip_comments_keep_len(src) + i, n, last = 0, len(m), 0 + bounds = [] + while i < n: + c = m[i] + if c in ";}": + last = i + 1; bounds.append(last); i += 1; continue + if c == "#" and m[m.rfind("\n", 0, i) + 1:i].strip() == "": + # preprocessor line (with continuations) is a boundary, never part of an item + j = i + while True: + e = m.find("\n", j) + if e < 0: e = len(m); break + if m[e - 1] == "\\": j = e + 1; continue + break + i = e; last = e; bounds.append(last); continue + if c == "(" : + i = match_close(m, i, "(", ")") + 1; continue + if c == "{": + head = m[last:i] + if not head.strip().endswith(")") and re.fullmatch(r"\s*([^;{}()#]+;\s*)*", head) and bounds: + # K&R definition: `type name(a, b) T a; U b; {` -- the header ends at an earlier ';' + for b in reversed(bounds): + h2 = m[b:i] + if re.fullmatch(r"\s*[^;{}#]*?\w+\s*\(\s*\w+(\s*,\s*\w+)*\s*\)\s*([^;{}()#]+;\s*)+", h2): + last = b; head = h2; break + if b < i - 2000: break + bounds_snapshot = None + j = match_close(m, i) + hs = head.strip() + is_func = hs.endswith(")") or re.search(r"\)\s*([^;{}()#]+;\s*)+$", hs) or re.search(r"\)\s*(__attribute__\s*\(\(.*\)\))\s*$", hs, re.S) + if is_func and not re.match(r"\s*(typedef|struct|union|enum)\b", head) and "=" not in head.split("(")[0]: + # name = identifier before the parameter list + p = hs.rfind(")"); depth = 0; k = p + while k >= 0: + if hs[k] == ")": depth += 1 + elif hs[k] == "(": + depth -= 1 + if depth == 0: break + k -= 1 + name = IDENT.findall(hs[:k])[-1] + yield ("func", last, j + 1, (name, i)) + last = j + 1; bounds.append(last); i = j + 1 + continue + i = j + 1; last = i; bounds.append(last); continue + if m.startswith(("__asm__", "asm"), i) and (i == 0 or not (m[i - 1].isalnum() or m[i - 1] == "_")): + w = IDENT.match(m, i).group(0) + if w in ("__asm__", "asm", "__asm") and m[last:i].strip() == "": + k = m.index("(", i); j = match_close(m, k, "(", ")") + e = m.index(";", j) + 1 + yield ("asm", i, e, None) + last = e; i = e; continue + i += 1 + + +def rewrite_types(s): + s = re.sub(r"typedef\s+[\w ]+?\s+(\w+)\s+__attribute__\s*\(\(\s*mode\s*\(\s*TI\s*\)[^;]*?\)\)\s*;", + r"typedef unsigned int \1 __attribute__((ext_vector_type(4), aligned(16)));", s) + # long -> long long, leaving existing "long long" alone + s = re.sub(r"\blong(\s+long)?\b", lambda q: q.group(0) if q.group(1) else "long long", s) + # and the literal suffixes: 0x8000UL << 45 is a 64-bit value on the EE + s = re.sub(r"\b(0[xX][0-9A-Fa-f]+|[0-9]+)([uU]?)[lL]\b(?![lL])", lambda q: q.group(1) + q.group(2) + "LL", s) + s = re.sub(r"\b(0[xX][0-9A-Fa-f]+|[0-9]+)[lL][uU]\b", lambda q: q.group(1) + "ULL", s) + return s + + +DECL = re.compile(r"^[ \t]*(?:extern[ \t]+)?(?P[A-Za-z_][\w \t\*]*?)[ \t\*]*\b(?P(?:func|D)_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)[ \t]*(?P\((?:[^;{}]*)\)|\[[^;{}]*\])?[ \t]*;", re.M) + + +MAIN_TEXT = (0x001E9080, 0x0023E740) # the executable's game program (.text of the main segment) +RESIDENT = 0x0015F000 # below: core segment and core_lit, the same in every program +PLACES = {} # level mode: catalogue name -> its addresses in this level +CAT_KIND = {} +CAT_SIZE = {} +if LEVEL is not None: + for _l in open(os.path.join(REPO, "config", "overlays", "functions.tsv")): + if _l.startswith("#") or not _l.strip(): continue + _f = _l.rstrip("\n").split("\t") + CAT_KIND[_f[0]] = _f[1] + CAT_SIZE[_f[0]] = int(_f[2]) if _f[2].isdigit() else 0 + _p = [int(x.split(":")[1], 16) for x in _f[5].split(",") if x.split(":")[0] == LV] + if _p: PLACES[_f[0]] = _p +PLACE_ADDRS = {a for v in PLACES.values() for a in v} + + +# the level program's code (build/levels/level_NN/text.bin), for checking C against it +LTEXT = None +CALL_MISMATCH = [] +if LEVEL is not None: + try: + _man = json.load(open(os.path.join(HERE, "build", "levels", "level_" + LV, "manifest.json"))) + _tr = [r for r in _man["records"] if r["name"] == "text"][0] + LTEXT = (_tr["address"], open(os.path.join(HERE, "build", "levels", "level_" + LV, "text.bin"), "rb").read()) + except Exception as _e: + print("level text not found for call checks:", _e) +def level_call_targets(a, size): + base, d = LTEXT + if not (base <= a and a + size <= base + len(d)): return None + out = set() + for o in range(a - base, a - base + size, 4): + w = int.from_bytes(d[o:o + 4], "little") + if (w >> 26) in (2, 3): out.add(((a & 0xF0000000) | ((w & 0x3FFFFFF) << 2))) + return out + +def is_main_func(name): + """an executable main-segment function name (its copy differs per program)""" + q = re.fullmatch(r"func_([0-9A-Fa-f]{8})", name) + return bool(q) and MAIN_TEXT[0] <= int(q.group(1), 16) < MAIN_TEXT[1] + + +def base_func(name): + q = re.match(r"(func_(?:L\d\d_)?[0-9A-Fa-f]{8})", name) + return q.group(1) if q else None + + +def ps2_addr(name): + q = re.match(r"(func|D)_(?:L(\d\d)_)?([0-9A-Fa-f]{6,8})", name) + if not q: return None + a = int(q.group(3), 16) + if LEVEL is None: + return None if q.group(2) else a + if q.group(1) == "D": + return a + if q.group(2) or (len(q.group(3)) == 8 and MAIN_TEXT[0] <= a < MAIN_TEXT[1]): + p = PLACES.get(base_func(name)) + if p: return p[0] + # func_LNN_X naming another place of a function the level has (identical copies) + return a if q.group(2) == LV and a in PLACE_ADDRS else None + return a + + +ALIASES = {} # new name -> PS2 address + + +def split_conflicts(text, defined_here): + """gcc 2.95 accepts a function or global declared with different types in + one file (each localdecomp block brings its own prototypes); clang does + not. A redeclaration that differs from the first one (or from the + definition) gets its own name, used up to the next redeclaration; the new + name is an absolute symbol at the same PS2 address.""" + m = strip_comments_keep_len(text) + # top-level only: compute brace depth per position + depth = [0] * (len(m) + 1); d = 0 + for i, ch in enumerate(m): + depth[i] = d + if ch == "{": d += 1 + elif ch == "}": d -= 1 + first = {} + # definitions count as the first declaration + for q in re.finditer(r"^[^\n;{}]*?\b((?:func)_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)[ \t]*\(([^;{}]*)\)\s*(?:[^;{}()#]+;\s*)*\{", m, re.M): + if depth[q.start()] == 0: first.setdefault(q.group(1), "def:" + " ".join(q.group(0).split())) + edits = [] # (decl_start, decl_end, name, newname) + count = {} + for q in DECL.finditer(m): + if depth[q.start()] != 0 or "typedef" in q.group("t") or "return" in q.group("t"): continue + if "__asm__" in (q.group("s") or ""): continue # an asm-label alias, not a redeclaration + n = q.group("n"); sig = " ".join((q.group("t") + "|" + (q.group("s") or "")).split()) + if n not in first: + first[n] = sig; continue + if first[n] == sig and not first[n].startswith("def:"): continue + count[n] = count.get(n, 0) + 1 + nn = "%s__%sh%d" % (n, SFX[1:], count[n]) if SFX else "%s__h%d" % (n, count[n]) + a = ps2_addr(n) + if a is None: continue + ALIASES[nn] = a + edits.append((q.start("n"), q.end("n"), n, nn, q.end())) + # (a) a call before any declaration is an implicit `int f()`; if the file + # later declares or defines f differently, give those early calls their own name + declpos = {} + for q in DECL.finditer(m): + if depth[q.start()] == 0: declpos.setdefault(q.group("n"), q.start()) + for q in re.finditer(r"^[^\n;{}]*?\b((?:func)_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)[ \t]*\(([^;{}]*)\)\s*(?:[^;{}()#]+;\s*)*\{", m, re.M): + if depth[q.start()] == 0: declpos[q.group(1)] = min(declpos.get(q.group(1), 1 << 30), q.start()) + early = [] + for q in re.finditer(r"\b(func_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)\s*\(", m): + n = q.group(1) + if n in declpos and q.start() < declpos[n] and depth[q.start()] > 0 and ps2_addr(n) is not None: + early.append((q.start(1), len(n), n + "__%simp" % SFX[1:])) + ALIASES[n + "__%simp" % SFX[1:]] = ps2_addr(n) + if early: + names = sorted({e[2] for e in early}) + edits_early = early + else: + edits_early = [] + if not edits and not edits_early: return text + # each renamed decl renames uses of n up to the next decl of n (or end of file) + by_name = {} + for e in edits: by_name.setdefault(e[2], []).append(e) + starts = {} + for q in DECL.finditer(m): + if depth[q.start()] == 0: starts.setdefault(q.group("n"), []).append(q.start()) + repl = [] # (pos, len, new) + for n, es in by_name.items(): + for (ns, ne, _, nn, dend) in es: + later = [p for p in starts.get(n, []) if p > ns] + stop = later[0] if later else len(m) + repl.append((ns, ne - ns, nn)) + for u in re.finditer(r"\b%s\b" % re.escape(n), m[dend:stop]): + p = dend + u.start() + # do not rename the in-file definition itself + tail = m[p + len(n):p + len(n) + 400] + if depth[p] == 0 and re.match(r"\s*\([^;{}]*\)\s*(?:[^;{}()#]+;\s*)*\{", tail): continue + repl.append((p, len(n), nn)) + repl += edits_early + repl.sort(reverse=True) + t = text + for p, l, new in repl: + t = t[:p] + new + t[p + l:] + if edits_early: + hdr = "".join("extern int %s();\n" % nm for nm in sorted({e[2] for e in edits_early})) + k = t.find("\n", t.find('#include')) + 1 + t = t[:k] + hdr + t[k:] + return t + + +def ret_type(head, name): + t = head[:head.rfind(name)] + t = re.sub(r"/\*.*?\*/", " ", t, flags=re.S) + t = re.sub(r"\b(static|__inline__|inline|__inline|extern)\b", " ", t) + t = re.sub(r"__attribute__\s*\(\(.*?\)\)", " ", t) + return " ".join(t.split()) + + +# ---- headers: gen/include/ ------------------------------------------------ +# common.h is the host's own (host/include/rac1_common.h); include_asm.h is +# empty; the decomp's other headers get the same type rewrites as the C. +INC = os.path.join(OUT, "include") +if LEVEL is None: os.makedirs(INC, exist_ok=True) +for h in (glob.glob(os.path.join(REPO, "include", "*.h")) if LEVEL is None else []): + bn = os.path.basename(h) + if bn == "common.h": + t = open(os.path.join(HERE, "host", "include", "rac1_common.h")).read() + elif bn == "include_asm.h": + t = "/* host: assembly inclusion has no meaning on the PC */\n" + else: + t = rewrite_types(open(h, errors="replace").read()) + if PLAT == "windows": + t = re.sub(r'(__asm__|__asm|asm)(\s*\(\s*")((?:func|D)_\w+"\s*\))', r'\1\2_\3', t) + open(os.path.join(INC, bn), "w").write(t) + +stubbed, defined, asm_entries, report = [], set(), [], {} +gen_texts = {} +drafts = {os.path.basename(p)[:-2]: p for p in glob.glob(os.path.join(HERE, "host", "drafts", "func_*.c"))} +used_drafts = set() +files = sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "game", "*.c"))) +files += sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "game", "movie", "*.c"))) +files += ["src/core/989snd.c", "src/core/wad.c", "src/core/permcb.c"] +if LEVEL is not None: + # the level's program: shared level code and the level's own + files = sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "overlays", "shared", "*.c"))) + files += sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "overlays", "l%s_*" % LV, "*.c"))) +files = [f.replace(os.sep, "/") for f in files] +dropped = [] # level mode: functions of other levels' programs +# tools/source_patches.py: small text fixes where the decomp's C is right for +# the EE ABI but not for x86 (each entry: file, old text, new text) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from source_patches import PATCHES +patches_applied = 0 +# names.h: readable names are macros for the address names; expand them in +# the C (outside string literals) so every later pass sees func_/D_ names. +NAMES = {} +for q in re.finditer(r"^#define\s+(\w+)\s+((?:func|D)_\w+)\s*$", open(os.path.join(REPO, "include", "names.h")).read(), re.M): + NAMES[q.group(1)] = q.group(2) +NAMES_RE = re.compile(r"\b(%s)\b" % "|".join(map(re.escape, sorted(NAMES, key=len, reverse=True)))) if NAMES else None + + +def expand_names(src): + if not NAMES_RE: return src + m = strip_comments_keep_len(src) + out, pos = [], 0 + for q in NAMES_RE.finditer(m): + out.append(src[pos:q.start()]); out.append(NAMES[q.group(1)]); pos = q.end() + out.append(src[pos:]) + return "".join(out) + + +for rel in files: + src = expand_names(open(os.path.join(REPO, rel), errors="replace").read()) + for pf, old, new in PATCHES: + if os.path.basename(rel) == pf: + if old in src: + src = src.replace(old, new); patches_applied += 1 + else: + print("source patch not found in %s: %s" % (pf, old[:60])) + # host/drafts/.c: C for a function the decomp still has in assembly + # (a near miss is fine, it only has to behave the same). It is pasted where + # the INCLUDE_ASM line was, so it sees the same file context. + def use_draft(mm): + nm = re.findall(r"func_\w+", mm.group(2)) + if mm.group(1) == "INCLUDE_ASM" and nm and nm[-1] in drafts: + used_drafts.add(nm[-1]) + return "\n/* host: draft for %s */\n%s\n" % (nm[-1], open(drafts[nm[-1]], errors="replace").read()) + return mm.group(0) + src = ASM_MACRO.sub(use_draft, src) + for mm in ASM_MACRO.finditer(src): + kind, args = mm.group(1), mm.group(2) + nm = re.findall(r"func_\w+", args) + if kind in ("INCLUDE_ASM", "ASM_FUNC") and nm: + if LEVEL is not None and nm[-1] not in PLACES: dropped.append(nm[-1]); continue + asm_entries.append(nm[-1]) + src = ASM_MACRO.sub("", src) + # MMI plzcw (leading sign bits) written as inline asm: a host helper + src = re.sub(r'\b(?:__asm__|__asm|asm)\s*(?:__volatile__|volatile)?\s*\(\s*"plzcw\s+%0\s*,\s*%1"\s*:\s*"=r"\s*\(([^()]+)\)\s*:\s*"r"\s*\(((?:[^()]|\([^()]*\))+)\)\s*\)\s*;', + r"\1 = uya_plzcw((u32)(\2));", src) + # matching-only asm that means nothing on the host: register pins + # (`register s32 x __asm__("$16")`) and empty barriers + src = re.sub(r'(\bregister\b[^;=(){}]*?)\s*(?:__asm__|__asm|asm)\s*\(\s*"\$\w+"\s*\)', r"\1", src) + src = re.sub(r'\b(?:__asm__|__asm|asm)\s*(?:__volatile__|volatile)?\s*\(\s*""\s*(?::[^;]*)?\)\s*;', '__asm__ __volatile__("" ::: "memory");', src) + # scheduling-only asm: nops (and .set directives) with no operands + src = re.sub(r'\b(?:__asm__|__asm|asm)\s*(?:__volatile__|volatile)?\s*\(\s*((?:"(?:\s|nop|\\n|\\t|;|\.set\s+\w+)*"\s*)+)(?::[^;()]*)?\)\s*;', '__asm__ __volatile__("" ::: "memory");', src) + items = list(top_level_items(src)) + # C names that are asm-label aliases of an address name (void f_r(...) __asm__("func_X");) + lab = {q.group(1): q.group(2) for q in re.finditer(r"\b(\w+)\s*\((?:[^;{}()]|\([^()]*\))*\)\s*__asm__\s*\(\s*\"(func_(?:L\d\d_)?[0-9A-Fa-f]{8})\"\s*\)\s*;", src)} + funcs = {x[3][0]: x for x in items if x[0] == "func"} + masked = strip_comments_keep_len(src) + # macros that expand to inline asm count as asm too + asm_macros = set() + for q in re.finditer(r"^[ \t]*#[ \t]*define[ \t]+(\w+)((?:[^\n]*\\\n)*[^\n]*)", src, re.M): + if ASM_RE.search(q.group(2)): asm_macros.add(q.group(1)) + am = re.compile(r"\b(%s)\b" % "|".join(map(re.escape, sorted(asm_macros)))) if asm_macros else None + has_asm = {nm for nm, x in funcs.items() if ASM_RE.search(masked[x[3][1]:x[2]]) or (am and am.search(masked[x[3][1]:x[2]]))} + # Level programs: a C function whose calls do not match its bytes (it calls a + # function the bytes never call: the C was written for another level's copy, or + # an identical-looking wrapper got the wrong name) is translated from the bytes. + if LEVEL is not None and LTEXT is not None: + for nm, x in funcs.items(): + b = base_func(nm) + if not b or nm in has_asm or b not in PLACES or not CAT_SIZE.get(b): continue + body = masked[x[3][1]:x[2]] + callees = set(re.findall(r"\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8})\w*\s*\(", body)) + callees |= {lab[c] for c in re.findall(r"\b(\w+)\s*\(", body) if c in lab} + bad = [] + for a0 in PLACES[b][:1]: + tg = level_call_targets(a0, CAT_SIZE[b]) + if tg is None: continue + for c in callees: + if base_func(c) == b or not c.startswith("func_L"): continue # the main program's functions have copies under other names + q = ps2_addr(c) # the address the host's call goes to + if q is None: continue + if q not in tg: bad.append(c) + if bad: + has_asm.add(nm); CALL_MISMATCH.append((nm, sorted(bad))) + # testing (level programs): UYA_FORCE_ASM="file-substring,..." or "re:regex" translates + # those functions from the level's bytes instead of compiling their C (to find a bad C function) + # By default every level function is translated from the level's bytes + # ("re:."): much of the overlay C is adapted and does not match them (the + # hero files help_0020CDF0.c/help_00232560.c break Ratchet's walking, others + # the right-stick camera and the camera's distance, a stray triangle). + # UYA_FORCE_ASM="" compiles all the C; or a list: file substrings, re:regex, + # !file (keep that file's C), e.g. "help_0020CDF0,help_00232560" (the old default). + _fa = os.environ.get("UYA_FORCE_ASM", "re:.") + if LEVEL is not None and _fa: + _ex = [t[1:] for t in _fa.split(",") if t.startswith("!")] + if any(x and x in rel for x in _ex): _fa = "" # "!file-substring": that file keeps its C + for tok in _fa.split(","): + if tok.startswith("!"): continue + kr = lambda nm: re.search(r"\)\s*[A-Za-z_][^{]*;\s*$", src[funcs[nm][1]:funcs[nm][3][1]].rstrip("{ \n\t")) # K&R parameters: kept as C + if tok.startswith("re:"): + has_asm |= {nm for nm in funcs if base_func(nm) == nm and re.search(tok[3:], nm) and not kr(nm)} + # C under another name with the function's asm label (impl_X(...) __asm__("func_LNN_X")): the same + has_asm |= {nm for nm in funcs if lab.get(nm) in PLACES and re.search(tok[3:], lab[nm]) and not kr(nm)} + elif tok and tok in rel: + has_asm |= {nm for nm in funcs if base_func(nm) == nm and not kr(nm)} + # propagate through same-file static helpers + statics = {nm for nm, x in funcs.items() if re.search(r"\bstatic\b", src[x[1]:x[3][1]])} + changed = LEVEL is None + while changed: + changed = False + for nm, x in funcs.items(): + if nm in has_asm: continue + body = masked[x[3][1]:x[2]] + if any(re.search(r"\b%s\s*\(" % h, body) for h in has_asm & statics): + has_asm.add(nm); changed = True + out, pos = [], 0 + for kind, s, e, extra in items: + out.append(src[pos:s]) + seg = src[s:e] + if kind == "asm": + seg = "/* host: top-level asm removed */" + else: + name, bpos = extra + if LEVEL is not None and base_func(name) == name and (name.startswith("func_L") or is_main_func(name)) and name not in PLACES: + dropped.append(name) + out.append("/* host: %s is not in level %s's program */" % (name, LV)); pos = e + continue + # C written for another level (a shared function's C names its + # canonical level's data, D_LNN_...) is not this level's: its bytes are used + foreign = LEVEL is not None and re.match(r"func_L(\d\d)_", name) and name[6:8] != LV + # static recompilation (RAC1_STATICRECOMP): every game function of the + # executable is translated from the game's bytes too; only the host's + # own implementations (host/src, host/overrides) stay + static_main = (LEVEL is None and STATIC and base_func(name) == name and name.startswith("func_") + and name not in host_defined and name not in overrides + and not re.search(r"\)\s*[A-Za-z_][^{]*;\s*$", src[s:bpos].rstrip("{ \n\t"))) + if static_main or (LEVEL is not None and (name in has_asm or foreign) and name not in overrides): + # inline asm (VU0, MMI...): the level's own bytes are translated + # instead (tools/recomp.py); the definition becomes a declaration + head = re.sub(r"\b(static|inline|__inline__|__inline)\b", " ", src[s:bpos]) + if re.search(r"\)\s*[A-Za-z_][^{]*;\s*$", head.rstrip("{ \n\t")): + head = head[:head.index("(")] + "()" # K&R parameters: an old-style declaration + out.append("/* host: %s %s: translated from the game's bytes */\n%s;" % ( + name, "static" if static_main else ("has C for another level" if foreign else "has inline asm"), head.rstrip())) + if lab.get(name) in PLACES: name = lab[name] # labelled C: the label is the function + asm_entries.append(name); report[name] = "static (recompiled)" if static_main else ("other level's C (recompiled)" if foreign else "inline asm (recompiled)") + pos = e + continue + if lab.get(name) in overrides: + seg = "/* host: %s (%s) is in host/overrides */" % (name, lab[name]) + report[lab[name]] = "override" + out.append(seg); pos = e + continue + if name in has_asm or name in overrides: + head = src[s:bpos] + rt = ret_type(head, name) + why = "override" if name in overrides else "inline asm" + if name in overrides: + seg = "/* host: %s is in host/overrides */" % name + else: + body = "{ uya_unimpl(\"%s\"); " % name + if rt != "void": + body += "{ %s uya_r; __builtin_memset(&uya_r, 0, sizeof uya_r); return uya_r; } " % rt + seg = head + body + "}" + stubbed.append(name) + report[name] = why + else: + report[name] = "c" + if name.startswith("func_"): defined.add(name) + out.append(seg) + pos = e + out.append(src[pos:]) + text = split_conflicts("".join(out), set(funcs)) + text = rewrite_types(text) + # asm labels (`extern T x __asm__("D_00137C80");`) name a symbol directly; + # COFF i386 (Windows) symbols carry a leading underscore + if PLAT == "windows": + text = re.sub(r'(__asm__|__asm|asm)(\s*\(\s*")((?:func|D)_\w+"\s*\))', r'\1\2_\3', text) + gen_texts[rel[len("src/"):].replace("/", "_")] = text + +# ---- argument pass-through ------------------------------------------------- +# On the EE, arguments travel in $a0-$a3. A function the decomp writes as +# `void f(T *o) { g(o); }` can still hand its caller's 2nd and 3rd arguments +# to g, because f never touches $a1/$a2. On x86 those are lost. Where a call +# passes fewer arguments than the callee's definition takes, the missing ones +# are the enclosing function's own incoming arguments at those positions; the +# enclosing function gets extra `s32 uya_aN` parameters when it has too few. +FLOATY = re.compile(r"\b(f32|float|double|f64)\b") +HDR = re.compile(r"^[^\n;{}#]*?\b(func_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)[ \t]*\(([^;{}]*)\)\s*((?:[^;{}()#]+;\s*)*)\{", re.M) + + +def split_args(t): + out, d, cur = [], 0, "" + for ch in t: + if ch in "([": d += 1 + elif ch in ")]": d -= 1 + if ch == "," and d == 0: out.append(cur); cur = "" + else: cur += ch + if cur.strip(): out.append(cur) + return [x.strip() for x in out] + + +def params_of(plist, krdecls): + ps = split_args(plist) + if ps == ["void"] or ps == []: return [] + res = [] + for x in ps: + if x == "...": res.append(("...", "...")); continue + if re.fullmatch(r"\w+", x): # K&R: type in the declarations + q = re.search(r"([^;]*?)\b%s\b[^;]*;" % re.escape(x), krdecls) + res.append((q.group(1) if q else "int", x)) + else: + q = re.search(r"(\w+)\s*(\[[^\]]*\])?\s*$", x) + res.append((x, q.group(1) if q else None)) + return res + + +def base_name(n): + return re.sub(r"__(?:l\d\d)?(h\d+|imp)$", "", n) + + +def top_headers(t): + """function definitions at file scope: (name, match) on the masked text""" + m = strip_comments_keep_len(t) + depth = [0] * (len(m) + 1); d = 0 + for i, ch in enumerate(m): + depth[i] = d + if ch == "{": d += 1 + elif ch == "}": d -= 1 + return m, [q for q in HDR.finditer(m) if depth[q.start()] == 0 and depth[q.end() - 1] == 0] + + +# 1. parameter lists of every definition +defs, info, rets = {}, {}, {} +for fn, t in gen_texts.items(): + m, hs = top_headers(t) + info[fn] = (m, hs) + for q in hs: + defs[q.group(1)] = params_of(t[q.start(2):q.end(2)], t[q.start(3):q.end(3)]) + rets[q.group(1)] = ret_type(t[q.start():q.start(1) + len(q.group(1))], q.group(1)) +# host functions (host/src, host/overrides) too: a decomp call that leaves out +# arguments the EE would still pass in registers gets them filled in +for hp in glob.glob(os.path.join(HERE, "host", "src", "**", "*.c"), recursive=True) + glob.glob(os.path.join(HERE, "host", "overrides", "*.c")): + ht = open(hp, errors="replace").read() + hm = strip_comments_keep_len(ht) + for q in HDR.finditer(hm): + if q.group(1) not in defs: + defs[q.group(1)] = params_of(ht[q.start(2):q.end(2)], ht[q.start(3):q.end(3)]) + rets[q.group(1)] = ret_type(ht[q.start():q.start(1) + len(q.group(1))], q.group(1)) +# level mode: the executable's definitions too (its main-segment functions +# have copies in the level; core functions are shared) +exe_def_names = set() +if LEVEL is not None and EXEGEN: + _d = json.load(open(os.path.join(EXEGEN, "defs.json"))) + for n, v in _d.items(): + if n not in defs: + defs[n] = [tuple(x) for x in v["params"]]; rets[n] = v["ret"]; exe_def_names.add(n) +# 2. short calls inside each definition +sites = {} # fn -> list of (enclosing, name_start, callee_text, open, close, k, callee_base) +for fn, t in gen_texts.items(): + m, hs = info[fn] + lst = [] + for q in hs: + bstart = q.end() - 1 + bend = match_close(m, bstart) + for c in re.finditer(r"(?= mm_ or mm_ > 8: continue + cp = defs[callee] + [("s32", "")] * (mm_ - len(defs[callee])) + if any(FLOATY.search(cp[i][0]) or "struct" in cp[i][0] for i in range(k, mm_)): continue + if any(defs[enc][i][1] is None for i in range(k, min(mm_, len(defs[enc])))): continue + if want[enc] < mm_: + want[enc] = mm_; ch = True + if not ch: break +# 4. edit: extend headers, widen short calls +passthru_edits = 0 +for fn, t in gen_texts.items(): + m, hs = info[fn] + edits = [] + for q in hs: + n = q.group(1); have = len(defs[n]); need = want[n] + if need <= have: continue + add = ["uya_a%d" % i for i in range(have, need)] + plist = t[q.start(2):q.end(2)] + if have == 0: + edits.append((q.start(2), q.end(2), ", ".join("s32 " + a for a in add))) + elif q.group(3).strip(): # K&R + edits.append((q.end(2), q.end(2), ", " + ", ".join(add))) + edits.append((q.end(3), q.end(3), " ".join("s32 %s;" % a for a in add) + " ")) + else: + edits.append((q.end(2), q.end(2), ", " + ", ".join("s32 " + a for a in add))) + for enc, cs, cname, op, cl, k, callee in sites[fn]: + mm_ = want[callee] + if k >= mm_ or mm_ > 8: continue + cp = defs[callee] + [("s32", "")] * (mm_ - len(defs[callee])) + if any(FLOATY.search(cp[i][0]) or "struct" in cp[i][0] for i in range(k, mm_)): continue + own = [x[1] for x in defs[enc]] + ["uya_a%d" % i for i in range(len(defs[enc]), want[enc])] + if mm_ > len(own) or any(x is None for x in own[k:mm_]): continue + orig = t[op + 1:cl] + full = orig + (", " if k else "") + ", ".join(own[k:mm_]) + rt = rets.get(callee, "") + if re.fullmatch(r"(void|int|unsigned int|unsigned|short|char|float|double|long long|unsigned long long|[su](8|16|32|64)|f32|f64)(\s*\*)*", rt): + edits.append((cs, op, "((%s (*)())%s)" % (rt, cname))) + else: + edits.append((cs, op, "((__typeof__(%s(%s)) (*)())%s)" % (cname, full, cname))) + edits.append((cl, cl, (", " if k else "") + ", ".join(own[k:mm_]))) + for st, en, txt in sorted(edits, key=lambda e: (e[0], e[1]), reverse=True): + t = t[:st] + txt + t[en:] + gen_texts[fn] = t + passthru_edits += len(edits) +print("argument pass-through edits:", passthru_edits) +# the extended definitions take the extra arguments (thunks pass them from the registers) +for n in list(defs): + if n in want and want[n] > len(defs[n]) and not any(x[0] == "..." for x in defs[n]) and n not in exe_def_names: + defs[n] = defs[n] + [("s32 uya_a%d" % i, "uya_a%d" % i) for i in range(len(defs[n]), want[n])] + +# ---- 64-bit pass-through parameters ---------------------------------------- +# A parameter the C declares as a 32-bit integer but only ever hands on, as a +# call argument, to a 64-bit parameter of another definition: on the EE the +# register carries all 64 bits from the caller to that call whatever the C +# says (retail moves it with daddu); on x86 the upper word is lost. So such a +# parameter becomes 64 bits. (func_001F69F0, FontPrintWindow's scaled-text +# path: the 64-bit TEX0 from GetEffectTex lost CBP/TCC/TH, so window titles -- +# "Options", "Language" on the main menu -- drew nothing.) +def _defs_by_addr(): + r = {} + for n, p in defs.items(): + if re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n) and not any(x[0] == "..." for x in p): + a = ps2_addr(n) + if a is not None: r[a] = p + return r +_DBA = _defs_by_addr() +_INTPAR = re.compile(r"\s*(?:const\s+)?(?:signed\s+|unsigned\s+)?(?:int|s32|u32|long|unsigned|signed)\s+(\w+)\s*") +_widened = [] +for fn in list(gen_texts): + t = gen_texts[fn] + m, hs = top_headers(t) + edits = [] + for q in hs: + name = q.group(1) + if q.group(3).strip(): continue # K&R + plist = t[q.start(2):q.end(2)] + parts = split_args(plist) + if not parts or parts == ["void"] or "..." in parts: continue + bstart = q.end() - 1 + bend = match_close(m, bstart) + body = m[bstart:bend] + wide = [] + for i, x in enumerate(parts): + pm = _INTPAR.fullmatch(x) + if not pm or re.search(r"\blong\s+long\b", x): continue + pn = pm.group(1) + occ = list(re.finditer(r"(?])%s\b" % re.escape(pn), body)) + if not occ: continue + ok, hit = True, False + for o in occ: + pre = body[:o.start()].rstrip(); post = body[o.end():].lstrip() + if not pre or pre[-1] not in "(," or not post or post[0] not in "),": + ok = False; break + # the call this argument belongs to: the innermost unclosed '(' + d, j = 0, o.start() - 1 + while j >= 0: + if body[j] == ")": d += 1 + elif body[j] == "(": + if d == 0: break + d -= 1 + j -= 1 + cm = re.search(r"\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w*)\s*$", body[:j]) if j >= 0 else None + if not cm: ok = False; break + cdef = _DBA.get(ps2_addr(cm.group(1))) + cl = match_close(body, j, "(", ")") + args = split_args(body[j + 1:cl]) + if cdef is None or len(args) != len(cdef): ok = False; break + k = [ai for ai, ax in enumerate(args) if re.fullmatch(r"\s*%s\s*" % re.escape(pn), ax)] + if not k: ok = False; break + for ai in k: + ct = cdef[ai][0][:-len(cdef[ai][1])] if cdef[ai][1] and cdef[ai][0].endswith(cdef[ai][1]) else cdef[ai][0] + if re.search(r"\blong\s+long\b|\b[su]64\b", ct) and "*" not in ct: hit = True + else: ok = False + if not ok: break + if ok and hit: wide.append((i, pn)) + if not wide: continue + newp = list(parts) + for i, pn in wide: newp[i] = "unsigned long long " + pn + edits.append((q.start(2), q.end(2), ", ".join(newp))) + if name in defs: + _wi = {i for i, _ in wide} + defs[name] = [("unsigned long long " + p[1], p[1]) if i in _wi else p for i, p in enumerate(defs[name])] + _widened.append("%s(%s)" % (name, ", ".join(pn for _, pn in wide))) + for st, en, txt in sorted(edits, reverse=True): + t = t[:st] + txt + t[en:] + gen_texts[fn] = t +print("64-bit pass-through parameters widened:", ", ".join(_widened) or "none") + +# ---- float/int argument order --------------------------------------------- +# On the EE, integer arguments go in $a0-$a3/$t0-$t3 and float arguments in +# $f12-$f19, each class in its own order. So the decomp can declare +# f(float, int) at a call site for a function defined as f(int, float), and on +# the PS2 both mean the same. On x86 only the position counts. Where a call +# goes through a prototype (or a cast) whose float and int parameters are the +# definition's in the same per-class order but interleaved differently, the +# call's arguments and that prototype are put in the definition's order. +def is_float(ty): + return bool(FLOATY.search(ty)) and "*" not in ty and "(" not in ty + +def class_perm(decl, defn): + """for each definition position, the decl index that supplies it (None: + the call has no argument there, it gets 0), or None if the two cannot be + matched class by class (float vs integer)""" + if len(decl) > len(defn) or any(x[0] == "..." for x in decl + defn): return None + di = [i for i, x in enumerate(decl) if not is_float(x[0])] + df = [i for i, x in enumerate(decl) if is_float(x[0])] + ei = [i for i, x in enumerate(defn) if not is_float(x[0])] + ef = [i for i, x in enumerate(defn) if is_float(x[0])] + if len(di) > len(ei) or len(df) > len(ef): return None + perm = [None] * len(defn) + for a, b in zip(ei, di): perm[a] = b + for a, b in zip(ef, df): perm[a] = b + return perm + +def size_class(ty): + if "*" in ty or "(" in ty: return "i" + if is_float(ty): return "f" + if re.search(r"\b(long\s+long|s64|u64)\b", ty): return "l" + if re.search(r"\bstruct\b|\b[A-Z]\w*_[0-9A-F]{6}\b|\bu128\b|\bQ_|\bV16", ty): return "s" + return "i" + +CANON = {"f": "f32", "l": "unsigned long long", "i": "s32"} + +fdefs = {} +for n, p in defs.items(): + a = ps2_addr(n) + if a is not None and re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n) and not any(x[0] == "..." for x in p): + fdefs[a] = p +for op_ in glob.glob(os.path.join(HERE, "host", "overrides", "func_*.c")): + ot = open(op_, errors="replace").read() + for q in HDR.finditer(strip_comments_keep_len(ot)): + n = q.group(1); a = ps2_addr(n) + if a is None or not re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n): continue + fdefs[a] = params_of(ot[q.start(2):q.end(2)], ot[q.start(3):q.end(3)]) +# definitions under another C name with an asm label to the address name +# (`void func_001F6668_r(...) __asm__("func_001F6668"); void func_001F6668_r(...) {`): +# the reference for the address's other prototypes too (FontPrint's 64-bit colour +# and texture were passed as 32-bit values by `(void *, ..., int, ...)` prototypes) +for fn_, t_ in gen_texts.items(): + for q in re.finditer(r"\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w+)\s*\((?:[^;{}()]|\([^()]*\))*\)\s*__asm__\s*\(\s*\"_?(func_(?:L\d\d_)?[0-9A-Fa-f]{8})\"\s*\)\s*;", + t_): + a = ps2_addr(q.group(2)) + if a is None or a in fdefs or q.group(1) not in defs or ps2_addr(q.group(1)) != a: continue + if any(x[0] == "..." for x in defs[q.group(1)]): continue + fdefs[a] = defs[q.group(1)] +PROTO = re.compile(r"\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w*)\s*\(((?:[^;{}()]|\([^()]*\))*)\)\s*(?:__asm__\s*\(\s*\"([^\"]*)\"\s*\)\s*)?;") +CAST = re.compile(r"\(\s*\(\s*[\w\s\*]+\(\s*\*\s*\)\s*\(((?:[^()]|\([^()]*\))*)\)\s*\)\s*(func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w*)\s*\)\s*\(") +reorder_edits = 0 +# Assembly-only functions have no definition: one of their declarations is +# made the reference instead (the longest plain extern prototype), the other +# prototypes are put in its float/int order, and recomp.py's C entry for the +# function takes it (sigs.json). Without this, `f(float, void *)` here and +# `f(void *, float)` there, the same call on the EE, reach the entry with the +# arguments swapped. +REF_DECL = {} +for fn, t_ in gen_texts.items(): + m_ = strip_comments_keep_len(t_) + for q in PROTO.finditer(m_): + if q.group(3): continue + a = ps2_addr(q.group(1)) + if a is None or a in fdefs: continue + if not re.search(r"\bextern\b[^;{}]*$", m_[max(0, q.start() - 200):q.start()]): continue + parts = [x for x in split_args(t_[q.start(2):q.end(2)]) if x != "void"] + if not parts or "..." in parts: continue + if a not in REF_DECL or len(parts) > len(REF_DECL[a]): REF_DECL[a] = parts +# 64-bit parameters of assembly-only functions: the reference's 32-bit integer +# at a position becomes 64 bits where another view of the function (a labeled +# prototype with as many parameters) or OpenRAC's near-miss C for it +# (nonmatching/*/func_X.c) has a 64-bit type there. On the EE the register +# carries 64 bits whatever the C says; on x86 the reference decides what the +# C entry takes (func_001F7070's colour and texture). +def _views64(): + v = {} + for fn_, t_ in gen_texts.items(): + for q in PROTO.finditer(t_): + if not q.group(3): continue + a = ps2_addr(q.group(3).lstrip("_")) + if a is None or ps2_addr(q.group(1)) != a: continue + v.setdefault(a, []).append([re.sub(r"\blong(?!\s+long)\b", "long long", x) for x in split_args(q.group(2)) if x != "void"]) + for p_ in glob.glob(os.path.join(REPO, "nonmatching", "*", "func_*.c")): + t_ = re.sub(r"\blong(?!\s+long)\b", "long long", open(p_, errors="replace").read()) # EE long: 64 bits + for q in re.finditer(r"\b(func_[0-9A-Fa-f]{8}\w*)\s*\(([^;{}()]*)\)\s*(?:__asm__\s*\(\s*\"(func_[0-9A-Fa-f]{8})\"\s*\))?\s*[;{]", t_): + a = ps2_addr(q.group(3) or q.group(1)) + if a is None: continue + v.setdefault(a, []).append([x for x in split_args(q.group(2)) if x != "void"]) + return v +_V64 = _views64() +_n64 = 0 +for a, parts in REF_DECL.items(): + for view in _V64.get(a, []): + if len(view) != len(parts) or "..." in view: continue + for i, x in enumerate(view): + vt = re.sub(r"\b\w+\s*$", "", x) if re.search(r"[\w\*\]]\s+\w+\s*$", x) and not re.fullmatch(r"\s*(unsigned|signed)?\s*\w+\s*", x) else x + if size_class(vt) == "l" and size_class(parts[i]) == "i": + parts[i] = "unsigned long long"; _n64 += 1 +print("64-bit parameters of assembly-only functions from other views:", _n64) +for a, parts in REF_DECL.items(): + fdefs[a] = [(x, None) for x in parts] + +def plan(types_text, a, labeled=False): + """(new parameter text, mapping) for a prototype or cast, or None when it + already agrees with the definition. mapping[i] is the argument index that + goes to definition position i (None: the call has none there; 0 is passed). + A prototype under its own name with an asm label (`f_x(...) __asm__("f")`) + is a deliberate view of the function: only its float/int order is fixed.""" + parts = [x for x in split_args(types_text) if x != "void"] + defn = fdefs[a] + if not parts or "..." in parts or not defn: return None + perm = class_perm([(x, None) for x in parts], defn) + if perm is None: return None + if labeled: + # a view keeps its own types, put in the definition's float/int order; a + # 32-bit integer where the definition has 64 bits is widened (the EE register + # carries all 64: FontPrint's texture from GetEffectTex) + if None in perm or len(perm) != len(parts): return None + vt = [] + for i, src in enumerate(perm): + dc, sc = size_class(fdefs[a][i][0]), size_class(parts[src]) + # and a 64-bit integer where the definition has 32 bits is narrowed: + # on x86 a wider argument shifts every later one (FontPrintCenter's + # colour, func_001F6F40, declared u64 in pause.c) + vt.append(CANON["l"] if (dc == "l" and sc == "i") else (CANON["i"] if (dc == "i" and sc == "l") else parts[src])) + if perm == list(range(len(parts))) and vt == parts: return None + return ", ".join(vt), perm + types = [] + for i, src in enumerate(perm): + dc = size_class(defn[i][0]) + if src is None: + if dc == "s": return None + types.append(CANON[dc]); continue + sc = size_class(parts[src]) + if "s" in (sc, dc) and sc != dc: return None + types.append(parts[src] if sc == dc else CANON[dc]) + if perm == list(range(len(parts))) and types == parts: return None + return ", ".join(types), perm + +PROTO_VIEW = re.compile(r"\b([A-Za-z_]\w*)\s*\(((?:[^;{}()]|\([^()]*\))*)\)\s*__asm__\s*\(\s*\"(_?func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w*)\"\s*\)\s*;") +n_view_fix = [0] +def is_proto_pre(pre): + """the text before a `name(...);` makes it a prototype: `extern ...`, or a + file-scope declaration without it (`E *func_L00_0026DEA0(V *, int, float, ...);` + at the start of a line: a call or a local declaration is indented)""" + if re.search(r"\bextern\b[^;{}]*$", pre): return True + return re.search(r"(?:^|\n)(?!return\b|else\b|case\b|do\b|goto\b)[A-Za-z_][\w \t\*]*$", pre) is not None +for fn, t in gen_texts.items(): + m = strip_comments_keep_len(t) + edits = [] + perms = {} + for q in PROTO.finditer(m): + a = ps2_addr(q.group(1)) + if a not in fdefs: continue + if q.group(3) and ps2_addr(t[q.start(3):q.end(3)].lstrip("_")) != a: continue # an asm label to another address + pre = m[max(0, q.start() - 200):q.start()] + if not is_proto_pre(pre): continue + r = plan(t[q.start(2):q.end(2)], a, labeled=bool(q.group(3))) + if r is None: continue + edits.append((q.start(2), q.end(2), r[0])) + if r[1] != list(range(len(r[1]))): perms[q.group(1)] = r[1] + # views under a name of their own (`float cb90_alt(float, int, float) + # __asm__("func_L00_0025CB90");`): the same fix, keyed by that name + for q in PROTO_VIEW.finditer(t): # t: the label is a string, blank in m + if m[q.start():q.end(1)] != q.group(1): continue # in a comment + if q.group(1).startswith("func_"): continue # done above + a = ps2_addr(t[q.start(3):q.end(3)].lstrip("_")) + if a not in fdefs: continue + pre = m[max(0, q.start() - 200):q.start()] + if not is_proto_pre(pre): continue + r = plan(t[q.start(2):q.end(2)], a, labeled=True) + if r is None: continue + edits.append((q.start(2), q.end(2), r[0])) + if r[1] != list(range(len(r[1]))): perms[q.group(1)] = r[1] + n_view_fix[0] += 1 + def fix_call(op, perm): + cl = match_close(m, op, "(", ")") + args = split_args(t[op + 1:cl]) + if len(args) != sum(1 for x in perm if x is not None): return + edits.append((op + 1, cl, ", ".join(args[i] if i is not None else "0" for i in perm))) + cast_spans = [] + for q in CAST.finditer(m): + a = ps2_addr(q.group(2)) + if a not in fdefs: continue + r = plan(t[q.start(1):q.end(1)], a) + if r is None: continue + edits.append((q.start(1), q.end(1), r[0])) + if r[1] != list(range(len(r[1]))): fix_call(q.end() - 1, r[1]) + cast_spans.append(q.start(2)) + for name, perm in perms.items(): + for c in re.finditer(r"(?])\b%s\s*\(" % re.escape(name), m): + if c.start() in cast_spans: continue + before = m[max(0, c.start() - 200):c.start()] + if is_proto_pre(before): continue # the prototype itself + fix_call(c.end() - 1, perm) + for st, en, txt in sorted(edits, key=lambda e: (e[0], e[1]), reverse=True): + t = t[:st] + txt + t[en:] + gen_texts[fn] = t + reorder_edits += len(edits) +if CALL_MISMATCH: + print("level %s: %d C functions call what their bytes do not; translated from the bytes instead:" % (LV, len(CALL_MISMATCH))) + for nm, bad in CALL_MISMATCH: print(" %s (calls %s)" % (nm, ", ".join(bad))) +print("prototype fixes (float/int order, 64-bit sizes):", reorder_edits, "; of views under their own names:", n_view_fix[0]) + +# ---- function addresses as values (level mode) ------------------------------ +# A function named as a value (a callback argument, a pointer stored in a +# moby) is its PS2 address on the PS2, and the game's code calls it, stores it +# and compares it as such. In C on the host the name is the host function; +# where the level's translated code or a table gets the value, it must be the +# PS2 address (which is a trampoline to the same host function, so a C call +# through it works too). So such uses become the PS2 address, typed as the +# function. Direct calls stay direct. +FVAL = re.compile(r"(&\s*)?\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8}\w*)\b") +fval_edits = 0 +if LEVEL is not None: + for fn, t in gen_texts.items(): + m = strip_comments_keep_len(t) + edits = [] + for q in FVAL.finditer(m): + nm = q.group(2) + a = ps2_addr(nm) + if a is None: continue + after = m[q.end():q.end() + 40].lstrip() + before = m[max(0, q.start() - 40):q.start()].rstrip() + if not after or after[0] == "(": continue # a call + if after[0] == ")" and after[1:].lstrip().startswith("("): continue # ((T (*)())f)(...): a call + if after[0] not in ";,)}:?+-=!" : continue + if not before or (before[-1] not in "=,({?:)!+-" and not re.search(r"\breturn$", before)): continue + edits.append((q.start(), q.end(), "((__typeof__(%s) *)0x%08X)" % (nm, a))) + for st, en, tx in sorted(edits, reverse=True): + t = t[:st] + tx + t[en:] + gen_texts[fn] = t + fval_edits += len(edits) + print("level %s: function values made PS2 addresses: %d" % (LV, fval_edits)) +for fn, t in gen_texts.items(): + open(os.path.join(OUT, fn), "w").write(t) + +for nm in overrides | host_defined: defined.add(nm) +# asm-only entries: one stub each (no prototype in scope, so any caller signature links) +with open(os.path.join(OUT, "asm_stubs.c"), "w") as f: + f.write('#include "uya_host.h"\n/* Functions still in assembly in the decomp. Replace one by adding host/overrides/.c. */\n') + for nm in asm_entries: + if nm in overrides or nm in defined: continue + f.write('long long %s(void) { uya_unimpl("%s"); return 0; }\n' % (nm, nm)) + report[nm] = "asm stub"; defined.add(nm) +for nm in overrides | host_defined: defined.add(nm) +# every function the generated C defines (whatever form its header takes) +for fn_, t_ in gen_texts.items(): + for q in top_headers(t_)[1]: + defined.add(q.group(1)) +# functions defined under another C name with an asm label to the address name: +# void func_00203B70_r(...) __asm__("func_00203B70"); void func_00203B70_r(...) { ... } +LABEL_ALIAS = {} +for fn_, t_ in gen_texts.items(): + m_ = strip_comments_keep_len(t_) + hdrs = {q.group(1) for q in top_headers(t_)[1]} + hdrs |= set(re.findall(r"^[^\n;{}#]*?\b(\w+)[ \t]*\([^;{}]*\)\s*\{", m_, re.M)) + for q in re.finditer(r"\b(\w+)\s*\((?:[^;{}()]|\([^()]*\))*\)\s*__asm__\s*\(\s*\"_?(func_(?:L\d\d_)?[0-9A-Fa-f]{8})\"\s*\)\s*;", t_): + if q.group(1) in hdrs and q.group(1) != q.group(2): + LABEL_ALIAS[q.group(2)] = q.group(1) + defined.add(q.group(2)) +# trampoline table: every host function with a PS2 address, so pointers to it work +TT = "uya_tramp_table" + SFX +with open(os.path.join(OUT, "tramp_table.c"), "w") as f: + f.write('/* generated by gen_host.py: PS2 address -> host function */\n#include "uya_runtime.h"\n') + tl = sorted((ps2_addr(n), n) for n in defined if ps2_addr(n) is not None and re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{6,8}", n)) + if LEVEL is not None: + # the level's code only (the core keeps the executable's), at every place it has + tl = sorted((a, n) for n in defined if n in PLACES for a in PLACES[n]) + seen_a = set(); rows = [] + for a, n in tl: + if a in seen_a: continue + seen_a.add(a); rows.append((a, n)) + for a, n in rows: f.write("extern void %s(void);\n" % n) + f.write("const uya_tramp_t %s[] = {\n" % TT) + for a, n in rows: f.write(" {0x%08X, (void *)%s},\n" % (a, n)) + f.write("};\nconst int %s = %d;\n" % (TT.replace("table", "count"), len(rows))) + +# absolute symbols +abs_syms = {} +for fn in ("config/symbol_addrs.txt", "config/port_symbols.txt"): + p = os.path.join(REPO, fn) + if not os.path.exists(p): continue + for l in open(p): + q = re.match(r"\s*(\w+)\s*=\s*(0x[0-9A-Fa-f]+)\s*;", l) + if q and q.group(1) not in abs_syms: abs_syms[q.group(1)] = int(q.group(2), 16) +if LEVEL is not None: + # the executable's names below the main segment only: the rest moved + abs_syms = {n: a for n, a in abs_syms.items() if a < RESIDENT and not is_main_func(n)} +abs_syms.update(ALIASES) +# anything else the generated C names by address (labels the decomp defines in its data asm) +for p in glob.glob(os.path.join(OUT, "*.c")) + glob.glob(os.path.join(OUT, "include", "*.h")): + if os.path.basename(p) in ("tramp_table.c",): continue + for nm in set(re.findall(r"(?= RESIDENT and re.fullmatch(r"D_[0-9A-F]{8}", nm): + print("level %s: warning: %s is an executable data name in the moved range" % (LV, nm)) + RENAME.add(nm); level_abs[nm] = a + # main-segment and level names the level C calls but the level's program does not have + missing = sorted(nm for nm in used if base_func(nm) == nm and nm not in host_main and (is_main_func(nm) or nm.startswith("func_L")) + and nm not in PLACES and nm not in lvl_defined and ps2_addr(nm) is None and nm not in overrides) + with open(os.path.join(OUT, "missing.c"), "w") as f: + f.write('#include "uya_host.h"\n/* names the level C uses that level %s\'s program does not have */\n' % LV) + for nm in missing: + f.write('long long %s(void) { uya_unimpl("%s (not in level %s)"); return 0; }\n' % (nm, nm, LV)) + RENAME |= set(missing) + # every main-segment function the executable has: the level's copy, if it has one + for u in json.load(open(os.path.join(REPO, "progress", "report.json")))["units"]: + for fn_ in u.get("functions", []): + if is_main_func(fn_["name"]) and fn_["name"] not in host_main: RENAME.add(fn_["name"]) + for nm in PLACES: + if nm not in host_main: RENAME.add(nm) + for nm in overrides: + if is_main_func(nm): RENAME.add(nm) + RENAME -= host_main + with open(os.path.join(OUT, "l%s_names.h" % LV), "w") as f: + f.write("/* generated by gen_host.py: level %s's names (forced into each of its C files) */\n" % LV) + for nm in sorted(RENAME): + f.write("#define %s %s%s\n" % (nm, nm, SFX)) + # asm labels name symbols directly: the same renames + lab_re = re.compile(r'((?:__asm__|__asm|asm)\s*\(\s*")(_?)((?:func|D)_\w+)("\s*\))') + for p in glob.glob(os.path.join(OUT, "*.c")): + tx = open(p).read() + t2 = lab_re.sub(lambda q: q.group(1) + q.group(2) + q.group(3) + (SFX if q.group(3) in RENAME else "") + q.group(4), tx) + if t2 != tx: open(p, "w").write(t2) + with open(os.path.join(OUT, "abs_syms.s"), "w") as f: + f.write("# Level %s: symbols its C places by address (data at its PS2 address in the level's program).\n" % LV) + for nm, a in sorted(level_abs.items()): + f.write(".globl %s%s%s\n.set %s%s%s, 0x%X\n" % (pre, nm, SFX, pre, nm, SFX, a)) + abs_syms = level_abs +with open(os.path.join(OUT, "abs_syms.s") if LEVEL is None else os.devnull, "w") as f: + f.write("# Symbols the decomp places by address (data at its PS2 address; calls go through trampolines).\n") + for nm, a in sorted(abs_syms.items()): + if nm in defined or nm in ("func_0",) or a == 0: continue + f.write(".globl %s%s\n.set %s%s, 0x%X\n" % (pre, nm, pre, nm, a)) +with open(os.path.join(OUT, "funcs.tsv"), "w") as f: + for nm in sorted(report): + f.write("%s\t%s\n" % (nm, report[nm])) +c = {} +for v in report.values(): c[v] = c.get(v, 0) + 1 +print("drafts used:", len(used_drafts), "of", len(drafts)) +print("functions:", c, "absolute symbols:", sum(1 for n in abs_syms if n not in defined)) + +# ---- signatures for tools/recomp.py ---------------------------------------- +# For every function with a PS2 address: its ABI classes (i: 32-bit int or +# pointer, l: 64-bit int, f: float, d: double, q: 128-bit, s: struct by +# value, v: void) from its definition (generated C, host code) or, for the +# assembly-only ones, from the declarations the C makes of them. +def abi_class(t): + t = " ".join(re.sub(r"\b(const|volatile|register|static|extern|inline|__inline__|signed)\b", " ", t or "").split()) + if not t or t == "void": return "v" + if "*" in t or "(" in t or "[" in t: return "i" + if re.search(r"\b(float|f32)\b", t): return "f" + if re.search(r"\b(double|f64)\b", t): return "d" + if re.search(r"\blong long\b|\b[su]64\b|\blong\b", t): return "l" + if re.search(r"\bu128\b|\bqword\b|ext_vector|__int128|\bU128\b|\bvec128\b", t): return "q" + if re.search(r"\b(struct|union)\b", t): return "s" + return "i" + +def param_type(p): + ty = p[0] + if p[1] and ty.endswith(p[1]): ty = ty[:-len(p[1])] + return ty + +for real, cname in LABEL_ALIAS.items(): + if cname in defs and real not in defs: + defs[real] = defs[cname]; rets[real] = rets.get(cname, "int") +sigs = {} +for n, ps in defs.items(): + a = ps2_addr(n) + if a is None: continue + src_ = "def" + if n in exe_def_names: + if n not in host_defined and n not in overrides: + if not is_main_func(n): continue # core: the executable pass's + src_ = "decl" # the level's copy is translated from its bytes + sigs[n] = {"addr": a, "ret": abi_class(rets.get(n, "int")), "params": [abi_class(param_type(p)) for p in ps if p[0] != "..."], + "var": any(p[0] == "..." for p in ps), "src": src_} + if src_ == "decl": sigs[n]["unk"] = False +DECLP = re.compile(r"\bextern\s+([^;{}()]*?)\b(func_(?:L\d\d_)?[0-9A-Fa-f]{8})(\w*)\s*\(((?:[^;{}()]|\([^()]*\))*)\)\s*(?:__asm__\s*\([^)]*\))?\s*;") +for fn_, t_ in gen_texts.items(): + m_ = strip_comments_keep_len(t_) + for q in DECLP.finditer(m_): + n = q.group(2) + if q.group(3) and not re.fullmatch(r"__h\d+|__imp|_\w+", q.group(3)): pass + if n in sigs and sigs[n]["src"] == "def": continue + plist = t_[q.start(4):q.end(4)].strip() + if plist in ("", "void"): ps = [] + else: ps = [x for x in split_args(plist)] + cls = [abi_class(re.sub(r"\b\w+\s*$", "", x) if re.search(r"[\w\*\]]\s+\w+\s*$", x) and not re.fullmatch(r"\s*(unsigned|signed)?\s*\w+\s*", x) else x) for x in ps if x != "..."] + cur = sigs.get(n) + cand = {"addr": ps2_addr(n), "ret": abi_class(q.group(1)), "params": cls, "var": "..." in ps, "src": "decl", "unk": plist == ""} + if cur is None or (cur.get("unk") and not cand["unk"]) or (not cand["unk"] and len(cand["params"]) > len(cur["params"])): sigs[n] = cand +def _decl_class(x): + return abi_class(re.sub(r"\b\w+\s*$", "", x) if re.search(r"[\w\*\]]\s+\w+\s*$", x) and not re.fullmatch(r"\s*(unsigned|signed)?\s*\w+\s*", x) else x) +for n, s in sigs.items(): + if s.get("src") == "decl" and s.get("addr") in REF_DECL: + s["params"] = [_decl_class(x) for x in REF_DECL[s["addr"]]]; s["var"] = False; s["unk"] = False +# A 64-bit return: the EE returns the whole $v0. If any declaration of an +# assembly-only function (plain or under another name with an asm label, e.g. +# `extern s64 get_effect_texture(s32) __asm__("func_001F4868")`) returns 64 +# bits, its C entry returns all 64 (callers declaring int still read the low +# half). Without this GetEffectTex's TEX0 lost its upper word (CBP, TCC, TH). +RET64 = re.compile(r"\bextern\s+([^;{}()]*?)\b(\w+)\s*\(((?:[^;{}()]|\([^()]*\))*)\)\s*(?:__asm__\s*\(\s*\"_?(func_(?:L\d\d_)?[0-9A-Fa-f]{8})\"\s*\))?\s*;") +for fn_, t_ in gen_texts.items(): + for q in RET64.finditer(strip_comments_keep_len(t_)): + n = q.group(4) or re.sub(r"^(func_(?:L\d\d_)?[0-9A-Fa-f]{8}).*$", r"\1", q.group(2)) + s = sigs.get(n) + if s is None or s.get("src") != "decl" or s["ret"] == "l": continue + if abi_class(q.group(1)) == "l" and s["ret"] in ("i", "v"): + s["ret"] = "l" +# ... and the C that declares such a function as returning int, or keeps its +# value in an int (`int mode = GetEffectTex(1);` then passed on as the texture), +# gets 64 bits: on the EE the register keeps all of them whatever the C says. +_r64 = sorted(n for n, s in sigs.items() if s.get("src") == "decl" and s["ret"] == "l" and re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n) + and (s.get("addr") or 0) >= 0x0015F000) # game code (the core's library calls are the host's) +if _r64: + _names = "|".join(re.escape(n) for n in _r64) + _INT = r"(?:int|s32|u32|unsigned int|unsigned|signed int|signed)" + _P1 = re.compile(r"(\bextern\s+|^[ \t]*)%s(\s+(?:%s)\w*\s*\([^;{}]*\)\s*;)" % (_INT, _names), re.M) + _P2 = re.compile(r"(\bextern\s+)%s(\s+\w+\s*\([^;{}]*\)\s*__asm__\s*\(\s*\"_?(?:%s)\"\s*\)\s*;)" % (_INT, _names)) + _P3 = re.compile(r"(^\s*)%s(\s+\w+\s*=\s*(?:%s)\w*\s*\()" % (_INT, _names), re.M) + _nfix = 0 + for fn_ in list(gen_texts): + p_ = os.path.join(OUT, fn_) + if not os.path.exists(p_): continue + t0 = open(p_).read() + t1, k1 = _P1.subn(r"\1long long\2", t0) + t1, k2 = _P2.subn(r"\1long long\2", t1) + t1, k3 = _P3.subn(r"\1long long\2", t1) + # `int glyphs; ... glyphs = GetEffectTex(kind);`: the nearest such declaration above + _again = True + while _again: + _again = False + for q in re.finditer(r"(?])(\w+)\s*=\s*(?:%s)\w*\s*\(" % _names, t1): + d_ = list(re.finditer(r"^([ \t]*)%s([ \t]+%s[ \t]*;)" % (_INT, re.escape(q.group(1))), t1[:q.start()], re.M)) + if d_: + d_ = d_[-1] + t1 = t1[:d_.start()] + d_.group(1) + "long long" + d_.group(2) + t1[d_.end():]; k3 += 1 + _again = True + break + if t1 != t0: + open(p_, "w").write(t1); gen_texts[fn_] = t1; _nfix += k1 + k2 + k3 + print("64-bit returns of assembly-only functions (%s): %d declarations and locals widened" % (", ".join(_r64), _nfix)) +json.dump(sigs, open(os.path.join(OUT, "sigs.json"), "w"), indent=0) +json.dump(sorted(set(asm_entries)), open(os.path.join(OUT, "asm_entries.json"), "w")) +if LEVEL is None: + # for the level passes: what this pass defines, and its definitions' parameters + json.dump(sorted(defined), open(os.path.join(OUT, "defined.json"), "w")) + json.dump({n: {"params": [list(x) for x in ps], "ret": rets.get(n, "int")} for n, ps in defs.items()}, + open(os.path.join(OUT, "defs.json"), "w")) +else: + print("level %s: %d functions of other programs left out; %d names renamed" % (LV, len(dropped), len(RENAME))) diff --git a/games/rac1/tools/level_dump.py b/games/rac1/tools/level_dump.py new file mode 100644 index 0000000..b3789eb --- /dev/null +++ b/games/rac1/tools/level_dump.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""level_dump.py OUT_DIR LEVEL [LEVEL...] [--iso ISO] [--repo DECOMP_REPO] + +Writes each level's program (the code overlay ParseBin loads over the +executable's main segment) to OUT_DIR/level_NN/: one file per record +(lit, bss, data, vtbl, camvtbl, sndvtbl, text) and manifest.json with the +load addresses and the entry point, the layout of OpenRAC's own dump +(tools/overlays.py dump, baserom/overlays/level_NN/). + +The bytes come from the user's own disc image, read the way the game finds +them: the table of contents at LBA 1500, the level's header, the header's +"data" sector range, whose first word is the offset of the code overlay +(docs/ASSETS.md, "Code overlays"). If the decomp repo already has an OpenRAC +dump of the level (baserom/overlays/level_NN/manifest.json), that is used +instead. Nothing here is ever committed: it is game data. + +recomp.py translates the level's assembly-only functions from text.bin. +""" +import json, os, shutil, struct, sys + +SECTOR = 2048 +TOC_LBA, TOC_SIZE = 1500, 0x2960 +LEVEL_TABLE = 0x28C8 # D_0013A548: (header LBA, size) per level +LEVEL_HEADER_SIZE = 0x2434 +RECORD_NAMES = ("lit", "bss", "data", "vtbl", "camvtbl", "sndvtbl", "text") + + +def read(f, lba, size): + f.seek(lba * SECTOR) + d = f.read(size) + if len(d) != size: + sys.exit("level_dump: short read from the disc image at LBA %d" % lba) + return d + + +def wad(raw, repo): + """the overlay is stored plain on PAL v2.00; a WAD-packed one is unpacked + with OpenRAC's decoder (tools/extract/formats.py)""" + sys.path.insert(0, os.path.join(repo, "tools", "extract")) + from formats import wad as unwad + return unwad(raw) + + +def overlay_records(raw): + """(entry point, [(address, size, type, bytes)]), as ParseBin (func_0012DA38) walks them""" + entry = struct.unpack_from("/rac1_wrapper.cfg)" % lv) + dump_from_iso(iso, lv, out, repo or "") + print("level %02d: read from %s" % (lv, iso)) + + +if __name__ == "__main__": + main() diff --git a/games/rac1/tools/recomp.py b/games/rac1/tools/recomp.py new file mode 100644 index 0000000..532780c --- /dev/null +++ b/games/rac1/tools/recomp.py @@ -0,0 +1,1029 @@ +#!/usr/bin/env python3 +"""recomp.py GEN_DIR DECOMP_REPO + +Ahead-of-time recompiler for the game's assembly-only functions. + +Each function the decomp still has as assembly (gen/asm_entries.json) is read +from the user's own executable and translated, instruction by instruction, +into a C function over an RcCtx (the EE's registers, host/include/rc.h). The +C is compiled with the rest of the program: nothing is translated or +interpreted at run time. + +Calls are bridged both ways: + - C -> recompiled: a wrapper with the function's C signature (from the + declarations the decomp's C makes of it, gen/sigs.json) loads the + arguments into a0-t3 / f12-f19, runs the translation and returns v0/f0. + It takes the function's own name, so it replaces the stub in asm_stubs.c + and gets the trampoline at the function's PS2 address. + - recompiled -> C: a thunk per C function reads the arguments from the + registers by its signature and calls it. + - recompiled -> recompiled: a direct C call sharing the register context. + - by address (jalr, function pointers): rc_dispatch() looks the address up. + +Writes GEN_DIR/rc_funcs.c, rc_wrappers.c, rc_table.c, and rewrites +asm_stubs.c without the functions it covers. + +recomp.py LEVEL_GEN_DIR DECOMP_REPO --level NN --dump DUMP_DIR --exe-gen EXE_GEN_DIR + +Level mode (docs/LEVELS.md): every function of level NN's program that the +level pass (gen_host.py --level) does not define in C is translated from the +level's own bytes (DUMP_DIR/level_NN/text.bin, tools/level_dump.py): its +assembly-only functions, the ones whose C has inline asm, and the copies of +the executable's game functions (their relocations are the level's own). +Functions and sizes come from config/overlays/functions.tsv; a function the +level has at several addresses is translated at each (func_X__lNNatADDR). +The generated symbols take the prefix rcNN_ (the executable's are rc_), the +dispatch table is rcNN_table, and the level's trampoline table +(uya_tramp_table_lNN) gets a row for every translated function. +""" +import glob, json, os, re, struct, sys + +import rabbitizer +rabbitizer.config.pseudos_enablePseudos = False + +GEN, REPO = sys.argv[1], sys.argv[2] +HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LEVEL = DUMP = EXEGEN = None +_a = sys.argv[3:] +while _a: + _o = _a.pop(0) + if _o == "--level": LEVEL = int(_a.pop(0)) + elif _o == "--dump": DUMP = _a.pop(0) + elif _o == "--exe-gen": EXEGEN = _a.pop(0) + else: sys.exit("recomp.py: unknown option " + _o) +LV = "%02d" % LEVEL if LEVEL is not None else None +P = "rc_" if LEVEL is None else "rc%s_" % LV # prefix of the generated symbols +RESIDENT = 0x0015F000 + +# ---- inputs ----------------------------------------------------------------- +# testing: UYA_RC_TRACE="func_A,func_B" (build time) logs every call of those translated functions +TRACE_FUNCS = set(x for x in os.environ.get("UYA_RC_TRACE", "").split(",") if x) +EXE = os.environ.get("UYA_EXE") or os.path.join(REPO, "baserom", "SCES_509.16") # build.py: the copy read from the disc image +img = open(EXE, "rb").read() +TEXT_LO, TEXT_HI = 0x00100080, 0x00100080 + 0x13E6B0 +LTEXT_LO = LTEXT_HI = 0 +if LEVEL is not None: + _ld = os.path.join(DUMP, "level_%s" % LV) + _man = json.load(open(os.path.join(_ld, "manifest.json"))) + _tr = [r for r in _man["records"] if r["name"] == "text"][0] + limg = open(os.path.join(_ld, "text.bin"), "rb").read() + b"\0" * 16 + LTEXT_LO, LTEXT_HI = _tr["address"], _tr["address"] + _tr["bytes"] + + +def word(a): + if LTEXT_LO <= a < LTEXT_HI + 16: + return struct.unpack_from("= 0 and _o % 4 == 0 and _o < LTEXT_HI - LTEXT_LO and _cat.get(LTEXT_LO + _o, _s) == _s: + _host_rows[LTEXT_LO + _o] = _n + print("level %s: %s (host) found at 0x%08X by its bytes" % (LV, _n, LTEXT_LO + _o)) + if _kind == "override": + with open(os.path.join(GEN, "overrides.txt"), "a") as _f: _f.write(_n + "\n") + _is_start |= set(_host_rows) + import bisect + # branches past a function's catalogued end into bytes no catalogued function + # covers (level 16's func_L16_002E7138 branches to 0x2E715C): the function is + # longer than the catalogue says, up to the next start + _changed = True + while _changed: + _changed = False + _spans = sorted((a, a + sz) for a, sz in fsize.values()) + _span_lo = [a for a, _ in _spans] + def _covered(v): + k = bisect.bisect_right(_span_lo, v) - 1 + return any(_spans[j][0] <= v < _spans[j][1] for j in range(max(0, k - 8), k + 1)) + for key, (a, sz) in list(fsize.items()): + for pc in range(a, a + sz, 4): + if not (LTEXT_LO <= pc < LTEXT_HI): break + w = struct.unpack_from("> 26, (_w >> 21) & 31, (_w >> 16) & 31, _w & 0xFFFF + if _op == 0x0F: _hi[_rt] = (_imm << 16, _i); continue # lui + if _op in (0x09, 0x0D) and _rs in _hi and _i - _hi[_rs][1] <= 64: # addiu / ori within 16 instructions + _v = (_hi[_rs][0] + (_imm - 0x10000 if (_op == 0x09 and _imm & 0x8000) else _imm)) & 0xFFFFFFFF + _cands.add(_v) + _starts_sorted = sorted((a, e) for a, e in _starts) + _lo_list = [a for a, _ in _starts_sorted] + _n_ptr = 0 + for _v in sorted(_cands): + if not (LTEXT_LO <= _v < LTEXT_HI) or _v % 4 or _v in _is_start: continue + _k = bisect.bisect_right(_lo_list, _v) - 1 + if _k < 0 or not (_starts_sorted[_k][0] < _v < _starts_sorted[_k][1]): continue + key = "func_l%s_entry_%08X" % (LV, _v) + fsize[key] = (_v, _starts_sorted[_k][1] - _v); base_of[key] = key; _is_start.add(_v); _n_ptr += 1 + print("level %s: %d function pointers into catalogued functions: entries of their own" % (LV, _n_ptr)) + # calls (jal) to an address that starts no catalogued function: an entry of + # their own, up to the next start + _sorted_starts = sorted(_is_start) + for _i in range(0, LTEXT_HI - LTEXT_LO, 4): + _w = struct.unpack_from("> 26 != 3: continue + _v = ((LTEXT_LO + _i + 4) & 0xF0000000) | ((_w & 0x3FFFFFF) << 2) + if not (LTEXT_LO <= _v < LTEXT_HI) or _v in _is_start: continue + _k = bisect.bisect_right(_sorted_starts, _v) + _end = _sorted_starts[_k] if _k < len(_sorted_starts) else LTEXT_HI + key = "func_l%s_entry_%08X" % (LV, _v) + fsize[key] = (_v, _end - _v); base_of[key] = key; _is_start.add(_v) + print("level %s: call target 0x%08X starts no catalogued function: translated as %s" % (LV, _v, key)) + exe_sigs = json.load(open(os.path.join(EXEGEN, "sigs.json"))) + lsigs = sigs + level_c = {n for n, s in lsigs.items() if s.get("src") == "def" and s.get("addr") is not None and s["addr"] >= RESIDENT + and re.fullmatch(r"func_(L\d\d_)?[0-9A-F]{8}", n)} + todo = [k for k in fsize if base_of[k] not in level_c] + # signatures: the level's, the executable's definitions of the core it calls + sigs = dict(lsigs) + for n, s in exe_sigs.items(): + if s.get("src") == "def" and s.get("addr") is not None and s["addr"] < RESIDENT: sigs[n] = s + c_funcs = {} + # a C function stands for each of its places only where the bytes are the + # same as at its first place: the catalogue's fingerprints leave out call + # targets, so tiny wrappers that call different functions share a name + # (func_L00_00237B70, the HUD's, is also at 0x0025B458, a colour blend's). + # The other places are translated from their own bytes. + _lb = lambda a, n_: bytes(limg[a - LTEXT_LO:a - LTEXT_LO + n_]) + _n_diff = 0 + for n in level_c: + pl = PLACES.get(n, [lsigs[n]["addr"]]) + for k, a in enumerate(pl): + if k and n in PLACES and LTEXT_LO <= a < LTEXT_HI and LTEXT_LO <= pl[0] < LTEXT_HI: + sz = fsize[n][1] + if _lb(a, sz) != _lb(pl[0], sz): + todo.append("%s__l%sat%08X" % (n, LV, a)); _n_diff += 1 + continue + c_funcs[a] = n + if _n_diff: print("level %s: %d places of C functions differ from the first place's bytes: translated" % (LV, _n_diff)) + for a, n in _host_rows.items(): + c_funcs[a] = n + if n not in sigs and n in exe_sigs: sigs[n] = exe_sigs[n] + for n, s in sigs.items(): + if s.get("src") == "def" and s.get("addr") is not None and s["addr"] < RESIDENT and re.fullmatch(r"func_[0-9A-F]{8}", n): + c_funcs.setdefault(s["addr"], n) + # the core's translated functions and thunks (the executable pass's): the level calls them directly + core_rc = {int(a, 16): f for a, f in re.findall(r"\{0x([0-9A-F]{8}), (rc_\w+)\},", open(os.path.join(EXEGEN, "rc_table.c")).read()) + if int(a, 16) < RESIDENT} +else: + c_funcs = {s["addr"]: n for n, s in sigs.items() if s.get("src") == "def" and re.fullmatch(r"func_[0-9A-F]{8}", n)} + core_rc = {} + # code addresses the C names as data (`m->f74 = D_0023B578`, a second + # entry point 0x68 into the assembly-only func_0023B510): an entry of + # their own, translated from there to the end of the function they fall in + _tset = set(todo) + _starts_x = sorted((fsize[n][0], fsize[n][0] + fsize[n][1]) for n in _tset) + _is_start_x = {a for a, _ in _starts_x} + _abs = os.path.join(GEN, "abs_syms.s") + _cand = {int(x, 16) for x in re.findall(r"\.set\s+_?D_([0-9A-F]{8})\w*,", open(_abs).read())} if os.path.exists(_abs) else set() + _cand = {v for v in _cand if 0x001E9080 <= v < 0x0023E740} + # and functions the C (the executable's and the levels') calls by an address + # inside an assembly-only function: func_001252C0 is 0x20 into libvu0's + # func_001252A0 (the levels' hero code calls it) + for _root, _ds, _fs in os.walk(os.path.join(REPO, "src")): + for _f in _fs: + if _f.endswith(".c"): + _cand |= {int(x, 16) for x in re.findall(r"\bfunc_([0-9A-F]{8})\b", open(os.path.join(_root, _f), errors="replace").read())} + # and core addresses the level programs call with jal inside a function (level 17 + # calls 0x125328, 0x88 into libvu0's func_001252A0): from the levels' code that + # build.py extracted (build/levels/level_NN/text.bin; the core, below 0x15F000, + # is the same in every program) + for _tb in sorted(glob.glob(os.path.join(HERE, "build", "levels", "level_*", "text.bin"))): + _d = open(_tb, "rb").read() + for _i in range(0, len(_d) - 3, 4): + _w = struct.unpack_from("> 26 == 3: + _t = (_w & 0x3FFFFFF) << 2 + if TEXT_LO <= _t < 0x0015F000: _cand.add(_t) + for _v in sorted(_cand): + if not (TEXT_LO <= _v < TEXT_HI) or _v % 4 or _v in _is_start_x: continue + _own = [(a, e) for a, e in _starts_x if a < _v < e] + if not _own: continue + key = "func_entry_%08X" % _v + fsize[key] = (_v, _own[0][1] - _v); base_of[key] = key; todo.append(key); _is_start_x.add(_v) + print("address 0x%08X named in the C is inside the function at 0x%08X: translated as %s" % (_v, _own[0][0], key)) +todo = sorted(set(todo), key=lambda n: fsize[n][0]) +rc_addr = {fsize[n][0]: n for n in todo} +# every known function start (for "is this a call to another function") +starts = {} +for n, (a, s) in fsize.items(): + if re.fullmatch(r"func_[0-9A-F]{8}", n): starts[a] = n +for n, s in sigs.items(): + if s.get("addr") is not None and re.fullmatch(r"func_[0-9A-F]{8}", n): starts.setdefault(s["addr"], n) + + +# ---- helpers ---------------------------------------------------------------- +def R(n): + return "0ULL" if n == 0 else "c->r[%d].d[0]" % n + + +def W(n, expr): + return "" if n == 0 else "c->r[%d].d[0] = %s;" % (n, expr) + + +def S16(w): + v = w & 0xFFFF + return v - 0x10000 if v & 0x8000 else v + + +def EA(w): + rs = (w >> 21) & 31 + return "(u32)(%s + %d)" % (R(rs), S16(w)) + + +warnings = {} + + +def warn(op, pc): + warnings.setdefault(op, []).append(pc) + + +DESTS = "xyzw" + + +def dest_of(w): + d = (w >> 21) & 15 + return [i for i in range(4) if d & (8 >> i)] + + +def vu_parts(w): + return (w >> 6) & 31, (w >> 11) & 31, (w >> 16) & 31 # fd, fs, ft + + +def vset(fd, comps, exprs): + """assign VF[fd][i] = exprs[i] for i in comps, via temporaries (sources may alias fd)""" + if fd == 0: + return "/* write to vf0 ignored */" + s = "{ " + " ".join("f32 t%d = %s;" % (i, exprs[i]) for i in comps) + s += " " + " ".join("VF(%d)[%d] = t%d;" % (fd, i, i) for i in comps) + " }" + return s + + +def aset(comps, exprs): + s = "{ " + " ".join("f32 t%d = %s;" % (i, exprs[i]) for i in comps) + s += " " + " ".join("VACC[%d] = t%d;" % (i, i) for i in comps) + " }" + return s + + +def vu_op(name, w, pc): + fd, fs, ft = vu_parts(w) + comps = dest_of(w) + bc = w & 3 + m = re.match(r"v(add|sub|mul|madd|msub|max|mini)(a?)(x|y|z|w|q|i)?$", name) + if m: + op, acc, src = m.group(1), m.group(2), m.group(3) + if acc: # special2 layout: no fd + pass + + def b(i): + if src in ("x", "y", "z", "w"): return "VF(%d)[%d]" % (ft, DESTS.index(src)) + if src == "q": return "rc_vu0.q" + if src == "i": return "rc_vu0.i" + return "VF(%d)[%d]" % (ft, i) + ex = {} + for i in comps: + a_, b_ = "VF(%d)[%d]" % (fs, i), b(i) + if op == "add": e = "rc_fc(%s + %s)" % (a_, b_) # rc_fc: the VU clamps to +-MAX + elif op == "sub": e = "rc_fc(%s - %s)" % (a_, b_) + elif op == "mul": e = "rc_fc(%s * %s)" % (a_, b_) + elif op == "madd": e = "rc_fc(VACC[%d] + rc_fc(%s * %s))" % (i, a_, b_) + elif op == "msub": e = "rc_fc(VACC[%d] - rc_fc(%s * %s))" % (i, a_, b_) + elif op == "max": e = "fmaxf(%s, %s)" % (a_, b_) + else: e = "fminf(%s, %s)" % (a_, b_) + ex[i] = e + if not acc and fd == 0: + # vf0 stays (0,0,0,1), but the op still sets the MAC/status flags: the game compares this way + # (`vsubw.w vf0, ...` then `cfc2 $at, $vi17; andi 0x10` skips point lights out of range) + if not NEED_VU0_FLAGS or op in ("max", "mini"): return "/* write to vf0 ignored */" + return "{ f32 vz[4] = { 0, 0, 0, 0 }; " + " ".join("vz[%d] = %s;" % (i, ex[i]) for i in comps) + \ + " rc_vu0_flags(vz, %du); }" % ((w >> 21) & 15) + code = aset(comps, ex) if acc else vset(fd, comps, ex) + if NEED_VU0_FLAGS and op not in ("max", "mini") and (acc or fd != 0): # FMAC ops set the MAC/status flags + code += " rc_vu0_flags(%s, %du);" % ("VACC" if acc else "VF(%d)" % fd, (w >> 21) & 15) + return code + if name == "vopmula": + ex = {0: "rc_fc(VF(%d)[1] * VF(%d)[2])" % (fs, ft), 1: "rc_fc(VF(%d)[2] * VF(%d)[0])" % (fs, ft), 2: "rc_fc(VF(%d)[0] * VF(%d)[1])" % (fs, ft)} + return aset([i for i in comps if i < 3], ex) + (" rc_vu0_flags(VACC, %du);" % (((w >> 21) & 15) & 14) if NEED_VU0_FLAGS else "") + if name == "vopmsub": + ex = {0: "rc_fc(VACC[0] - rc_fc(VF(%d)[1] * VF(%d)[2]))" % (fs, ft), 1: "rc_fc(VACC[1] - rc_fc(VF(%d)[2] * VF(%d)[0]))" % (fs, ft), 2: "rc_fc(VACC[2] - rc_fc(VF(%d)[0] * VF(%d)[1]))" % (fs, ft)} + cs = [i for i in comps if i < 3] + if fd == 0 and NEED_VU0_FLAGS: # vf0 stays; the flags are set (see the FMAC ops above) + return "{ f32 vz[4] = { 0, 0, 0, 0 }; " + " ".join("vz[%d] = %s;" % (i, ex[i]) for i in cs) + " rc_vu0_flags(vz, %du); }" % (((w >> 21) & 15) & 14) + return vset(fd, cs, ex) + ("" if fd == 0 or not NEED_VU0_FLAGS else " rc_vu0_flags(VF(%d), %du);" % (fd, ((w >> 21) & 15) & 14)) + if name in ("vmove", "vmr32", "vabs"): + # special2: ft is the destination, fs the source + if name == "vmove": ex = {i: "VF(%d)[%d]" % (fs, i) for i in comps} + elif name == "vabs": ex = {i: "__builtin_fabsf(VF(%d)[%d])" % (fs, i) for i in comps} + else: ex = {i: "VF(%d)[%d]" % (fs, (i + 1) & 3) for i in comps} + return vset(ft, comps, ex) + m = re.match(r"v(ftoi|itof)(0|4|12|15)$", name) + if m: + sh = int(m.group(2)) + if m.group(1) == "ftoi": + if ft == 0: return "/* ftoi to vf0 */" + return "{ " + " ".join("s32 t%d = rc_ftoi(VF(%d)[%d], %d);" % (i, fs, i, sh) for i in comps) + " " + \ + " ".join("__builtin_memcpy(&VF(%d)[%d], &t%d, 4);" % (ft, i, i) for i in comps) + " }" + ex = {} + for i in comps: + ex[i] = "rc_itof(*(s32 *)&VF(%d)[%d], %d)" % (fs, i, sh) + return vset(ft, comps, ex) + if name == "vclipw": + return ("{ f32 w_ = __builtin_fabsf(VF(%d)[3]); u32 fl = (VF(%d)[0] > w_) | ((VF(%d)[0] < -w_) << 1) | ((VF(%d)[1] > w_) << 2) | " + "((VF(%d)[1] < -w_) << 3) | ((VF(%d)[2] > w_) << 4) | ((VF(%d)[2] < -w_) << 5); " + "rc_vu0.clip = ((rc_vu0.clip << 6) | fl) & 0xFFFFFF; }") % (ft, fs, fs, fs, fs, fs, fs) + if name in ("vdiv", "vsqrt", "vrsqrt"): + fsf, ftf = (w >> 21) & 3, (w >> 23) & 3 + if name == "vdiv": return "rc_vu0.q = rc_fc(rc_fdiv(VF(%d)[%d], VF(%d)[%d]));" % (fs, fsf, ft, ftf) + if name == "vsqrt": return "rc_vu0.q = rc_fsqrt(VF(%d)[%d]);" % (ft, ftf) + return "rc_vu0.q = rc_fdiv(VF(%d)[%d], rc_fsqrt(VF(%d)[%d]));" % (fs, fsf, ft, ftf) + if name in ("vwaitq", "vnop"): return ";" + if name in ("vsqi", "vsqd", "vlqi", "vlqd"): + # vsqi fs, (it++): VU0 data memory at 0x11004000, 16 bytes per address unit + is_, it = (w >> 11) & 31, (w >> 16) & 31 + if name in ("vsqi", "vsqd"): + pre = "rc_vu0.vi[%d] = (rc_vu0.vi[%d] - 1) & 0xFFFF; " % (it, it) if name == "vsqd" and it else "" + post = " if (%d) rc_vu0.vi[%d] = (rc_vu0.vi[%d] + 1) & 0xFFFF;" % (it, it, it) if name == "vsqi" else "" + return "{ %sf32 *p = (f32 *)(uintptr_t)(0x11004000u + ((rc_vu0.vi[%d] & 0xFF) << 4)); %s%s }" % ( + pre, it, " ".join("p[%d] = VF(%d)[%d];" % (i, is_, i) for i in comps), post) + pre = "rc_vu0.vi[%d] = (rc_vu0.vi[%d] - 1) & 0xFFFF; " % (is_, is_) if name == "vlqd" and is_ else "" + post = " if (%d) rc_vu0.vi[%d] = (rc_vu0.vi[%d] + 1) & 0xFFFF;" % (is_, is_, is_) if name == "vlqi" else "" + if it == 0: return "/* vlq to vf0 */" + return "{ %sf32 *p = (f32 *)(uintptr_t)(0x11004000u + ((rc_vu0.vi[%d] & 0xFF) << 4)); %s%s }" % ( + pre, is_, " ".join("VF(%d)[%d] = p[%d];" % (it, i, i) for i in comps), post) + if name in ("viadd", "visub", "viand", "vior"): + id_, is_, it = (w >> 6) & 31, (w >> 11) & 31, (w >> 16) & 31 + op = {"viadd": "+", "visub": "-", "viand": "&", "vior": "|"}[name] + return "" if id_ == 0 else "rc_vu0.vi[%d] = (rc_vu0.vi[%d] %s rc_vu0.vi[%d]) & 0xFFFF;" % (id_ & 15, is_ & 15, op, it & 15) + if name == "viaddi": + it, is_, imm = (w >> 16) & 31, (w >> 11) & 31, (w >> 6) & 31 + if imm & 16: imm -= 32 + return "" if it == 0 else "rc_vu0.vi[%d] = (rc_vu0.vi[%d] + %d) & 0xFFFF;" % (it & 15, is_ & 15, imm) + if name == "vmtir": + it, fs_, fsf = (w >> 16) & 31, (w >> 11) & 31, (w >> 21) & 3 + return "" if it == 0 else "rc_vu0.vi[%d] = rc_fbits(VF(%d)[%d]) & 0xFFFF;" % (it & 15, fs_, fsf) + if name == "vmfir": + ft_, is_ = (w >> 16) & 31, (w >> 11) & 31 + return "" if ft_ == 0 else " ".join("{ s32 v = (s16)rc_vu0.vi[%d]; __builtin_memcpy(&VF(%d)[%d], &v, 4); }" % (is_ & 15, ft_, i) for i in comps) + if name in ("vcallms",): + return "rc_vcallms(c, 0x%X);" % (((w >> 6) & 0x7FFF) * 8) + if name in ("vcallmsr",): + return "rc_vcallms(c, (rc_vu0.cmsar0 & 0xFFFF) * 8);" # CMSAR0 (ctc2 $27 below) + warn(name, pc) + return 'rc_bad(0x%08X, "%s");' % (pc, name) + + +def mmi_op(name, w, pc): + rs, rt, rd, sa = (w >> 21) & 31, (w >> 16) & 31, (w >> 11) & 31, (w >> 6) & 31 + S, T = "c->r[%d]" % rs, "c->r[%d]" % rt + + def out(body): # body computes R128 x from s, t + if rd == 0: return "/* %s to $0 */" % name + return "{ R128 s = %s, t = %s, x; (void)s; (void)t; %s c->r[%d] = x; }" % (S if rs else "(R128){{0,0}}", T if rt else "(R128){{0,0}}", body, rd) + lanes = { + "pextlw": "x.w[0]=t.w[0]; x.w[1]=s.w[0]; x.w[2]=t.w[1]; x.w[3]=s.w[1];", + "pextuw": "x.w[0]=t.w[2]; x.w[1]=s.w[2]; x.w[2]=t.w[3]; x.w[3]=s.w[3];", + "pextlh": "for (int i=0;i<4;i++){x.h[2*i]=t.h[i]; x.h[2*i+1]=s.h[i];}", + "pextuh": "for (int i=0;i<4;i++){x.h[2*i]=t.h[4+i]; x.h[2*i+1]=s.h[4+i];}", + "pextlb": "for (int i=0;i<8;i++){x.b[2*i]=t.b[i]; x.b[2*i+1]=s.b[i];}", + "pextub": "for (int i=0;i<8;i++){x.b[2*i]=t.b[8+i]; x.b[2*i+1]=s.b[8+i];}", + "ppacw": "x.w[0]=t.w[0]; x.w[1]=t.w[2]; x.w[2]=s.w[0]; x.w[3]=s.w[2];", + "ppach": "for (int i=0;i<4;i++){x.h[i]=t.h[2*i]; x.h[4+i]=s.h[2*i];}", + "ppacb": "for (int i=0;i<8;i++){x.b[i]=t.b[2*i]; x.b[8+i]=s.b[2*i];}", + "pcpyld": "x.d[0]=t.d[0]; x.d[1]=s.d[0];", + "pcpyud": "x.d[0]=s.d[1]; x.d[1]=t.d[1];", + "pcpyh": "for (int i=0;i<4;i++){x.h[i]=t.h[0]; x.h[4+i]=t.h[4];}", + "pexew": "x.w[0]=t.w[2]; x.w[1]=t.w[1]; x.w[2]=t.w[0]; x.w[3]=t.w[3];", + "pexcw": "x.w[0]=t.w[0]; x.w[1]=t.w[2]; x.w[2]=t.w[1]; x.w[3]=t.w[3];", + "pexch": "x.h[0]=t.h[0];x.h[1]=t.h[2];x.h[2]=t.h[1];x.h[3]=t.h[3];x.h[4]=t.h[4];x.h[5]=t.h[6];x.h[6]=t.h[5];x.h[7]=t.h[7];", + "pexeh": "x.h[0]=t.h[2];x.h[1]=t.h[1];x.h[2]=t.h[0];x.h[3]=t.h[3];x.h[4]=t.h[6];x.h[5]=t.h[5];x.h[6]=t.h[4];x.h[7]=t.h[7];", + "prot3w": "x.w[0]=t.w[1]; x.w[1]=t.w[2]; x.w[2]=t.w[0]; x.w[3]=t.w[3];", + "pext5": "for (int i=0;i<4;i++){u32 v=t.w[i]; x.w[i]=((v&0x1F)<<3)|(((v>>5)&0x1F)<<11)|(((v>>10)&0x1F)<<19)|((v&0x8000)<<16);}", + "ppac5": "for (int i=0;i<4;i++){u32 v=t.w[i]; x.w[i]=((v>>3)&0x1F)|(((v>>11)&0x1F)<<5)|(((v>>19)&0x1F)<<10)|((v>>16)&0x8000);}", + "paddw": "for (int i=0;i<4;i++) x.w[i]=s.w[i]+t.w[i];", + "psubw": "for (int i=0;i<4;i++) x.w[i]=s.w[i]-t.w[i];", + "paddh": "for (int i=0;i<8;i++) x.h[i]=s.h[i]+t.h[i];", + "psubh": "for (int i=0;i<8;i++) x.h[i]=s.h[i]-t.h[i];", + "paddb": "for (int i=0;i<16;i++) x.b[i]=s.b[i]+t.b[i];", + "psubb": "for (int i=0;i<16;i++) x.b[i]=s.b[i]-t.b[i];", + "padduw": "for (int i=0;i<4;i++){u64 v=(u64)s.w[i]+t.w[i]; x.w[i]=v>0xFFFFFFFFu?0xFFFFFFFFu:(u32)v;}", + "paddsw": "for (int i=0;i<4;i++){s64 v=(s64)s.sw[i]+t.sw[i]; x.sw[i]=v>0x7FFFFFFF?0x7FFFFFFF:v<-0x7FFFFFFF-1?(-0x7FFFFFFF-1):(s32)v;}", + "psubsb": "for (int i=0;i<16;i++){int v=s.sb[i]-t.sb[i]; x.sb[i]=v>127?127:v<-128?-128:v;}", + "paddsb": "for (int i=0;i<16;i++){int v=s.sb[i]+t.sb[i]; x.sb[i]=v>127?127:v<-128?-128:v;}", + "paddub": "for (int i=0;i<16;i++){int v=s.b[i]+t.b[i]; x.b[i]=v>255?255:v;}", + "psubub": "for (int i=0;i<16;i++){int v=s.b[i]-t.b[i]; x.b[i]=v<0?0:v;}", + "paddsh": "for (int i=0;i<8;i++) x.sh[i]=rc_clamp16(s.sh[i]+t.sh[i]);", + "psubsh": "for (int i=0;i<8;i++) x.sh[i]=rc_clamp16(s.sh[i]-t.sh[i]);", + "pmaxw": "for (int i=0;i<4;i++) x.sw[i]=s.sw[i]>t.sw[i]?s.sw[i]:t.sw[i];", + "pminw": "for (int i=0;i<4;i++) x.sw[i]=s.sw[i]t.sh[i]?s.sh[i]:t.sh[i];", + "pminh": "for (int i=0;i<8;i++) x.sh[i]=s.sh[i]t.sw[i]?0xFFFFFFFFu:0;", + "pceqh": "for (int i=0;i<8;i++) x.h[i]=s.h[i]==t.h[i]?0xFFFF:0;", + "pcgth": "for (int i=0;i<8;i++) x.h[i]=s.sh[i]>t.sh[i]?0xFFFF:0;", + "pceqb": "for (int i=0;i<16;i++) x.b[i]=s.b[i]==t.b[i]?0xFF:0;", + "pcgtb": "for (int i=0;i<16;i++) x.b[i]=s.sb[i]>t.sb[i]?0xFF:0;", + "pand": "x.d[0]=s.d[0]&t.d[0]; x.d[1]=s.d[1]&t.d[1];", + "por": "x.d[0]=s.d[0]|t.d[0]; x.d[1]=s.d[1]|t.d[1];", + "pxor": "x.d[0]=s.d[0]^t.d[0]; x.d[1]=s.d[1]^t.d[1];", + "pnor": "x.d[0]=~(s.d[0]|t.d[0]); x.d[1]=~(s.d[1]|t.d[1]);", + "pabsw": "for (int i=0;i<4;i++) x.sw[i]=t.sw[i]<0?-t.sw[i]:t.sw[i];", + "pabsh": "for (int i=0;i<8;i++) x.sh[i]=t.sh[i]<0?-t.sh[i]:t.sh[i];", + "plzcw": "x=c->r[%d]; x.w[0]=(u32)rc_plzcw1(s.w[0]); x.w[1]=(u32)rc_plzcw1(s.w[1]);" % rd, + "psllw": "for (int i=0;i<4;i++) x.w[i]=t.w[i]<<%d;" % sa, + "psrlw": "for (int i=0;i<4;i++) x.w[i]=t.w[i]>>%d;" % sa, + "psraw": "for (int i=0;i<4;i++) x.sw[i]=t.sw[i]>>%d;" % sa, + "psllh": "for (int i=0;i<8;i++) x.h[i]=(u16)(t.h[i]<<%d);" % (sa & 15), + "psrlh": "for (int i=0;i<8;i++) x.h[i]=t.h[i]>>%d;" % (sa & 15), + "psrah": "for (int i=0;i<8;i++) x.sh[i]=t.sh[i]>>%d;" % (sa & 15), + "psllvw": "x.d[0]=SE32(t.w[0]<<(s.w[0]&31)); x.d[1]=SE32(t.w[2]<<(s.w[2]&31));", + "psrlvw": "x.d[0]=SE32(t.w[0]>>(s.w[0]&31)); x.d[1]=SE32(t.w[2]>>(s.w[2]&31));", + "psravw": "x.d[0]=SE32(t.sw[0]>>(s.w[0]&31)); x.d[1]=SE32(t.sw[2]>>(s.w[2]&31));", + "pinth": "for (int i=0;i<4;i++){x.h[2*i]=t.h[i]; x.h[2*i+1]=s.h[4+i];}", + "pinteh": "for (int i=0;i<4;i++){x.h[2*i]=t.h[2*i]; x.h[2*i+1]=s.h[2*i];}", + "prevh": "x.h[0]=t.h[3];x.h[1]=t.h[2];x.h[2]=t.h[1];x.h[3]=t.h[0];x.h[4]=t.h[7];x.h[5]=t.h[6];x.h[6]=t.h[5];x.h[7]=t.h[4];", + "pmultuw": "x.d[0]=(u64)s.w[0]*t.w[0]; x.d[1]=(u64)s.w[2]*t.w[2]; c->lo.d[0]=SE32((u32)x.d[0]); c->hi.d[0]=SE32((u32)(x.d[0]>>32)); c->lo.d[1]=SE32((u32)x.d[1]); c->hi.d[1]=SE32((u32)(x.d[1]>>32));", + "pmultw": "x.sd[0]=(s64)s.sw[0]*t.sw[0]; x.sd[1]=(s64)s.sw[2]*t.sw[2]; c->lo.d[0]=SE32((u32)x.d[0]); c->hi.d[0]=SE32((u32)(x.d[0]>>32)); c->lo.d[1]=SE32((u32)x.d[1]); c->hi.d[1]=SE32((u32)(x.d[1]>>32));", + "pmulth": "s32 p[8]; for (int i=0;i<8;i++) p[i]=(s32)s.sh[i]*t.sh[i]; x.sw[0]=p[0];x.sw[1]=p[2];x.sw[2]=p[4];x.sw[3]=p[6]; c->lo.sw[0]=p[0];c->lo.sw[1]=p[1];c->lo.sw[2]=p[4];c->lo.sw[3]=p[5]; c->hi.sw[0]=p[2];c->hi.sw[1]=p[3];c->hi.sw[2]=p[6];c->hi.sw[3]=p[7];", + } + if name in lanes: + if name in ("pmultuw", "pmultw", "pmulth") and rd == 0: + return "{ R128 s = %s, t = %s, x; %s }" % (S, T, lanes[name]) + return out(lanes[name]) + if name == "pmfhl.sh": + return out("x.sh[0]=rc_clamp16(c->lo.sw[0]); x.sh[1]=rc_clamp16(c->lo.sw[1]); x.sh[2]=rc_clamp16(c->hi.sw[0]); x.sh[3]=rc_clamp16(c->hi.sw[1]); " + "x.sh[4]=rc_clamp16(c->lo.sw[2]); x.sh[5]=rc_clamp16(c->lo.sw[3]); x.sh[6]=rc_clamp16(c->hi.sw[2]); x.sh[7]=rc_clamp16(c->hi.sw[3]);") + if name == "pmfhl.lw": + return out("x.w[0]=c->lo.w[0]; x.w[1]=c->hi.w[0]; x.w[2]=c->lo.w[2]; x.w[3]=c->hi.w[2];") + if name == "pmfhl.uw": + return out("x.w[0]=c->lo.w[1]; x.w[1]=c->hi.w[1]; x.w[2]=c->lo.w[3]; x.w[3]=c->hi.w[3];") + if name == "pmflo": return out("x=c->lo;") + if name == "pmfhi": return out("x=c->hi;") + if name == "pmtlo": return "c->lo = c->r[%d];" % rs + if name == "pmthi": return "c->hi = c->r[%d];" % rs + warn(name, pc) + return 'rc_bad(0x%08X, "%s");' % (pc, name) + + +# ---- one instruction -------------------------------------------------------- +def simple(ins, w, pc): + """C for an instruction that is not a branch or jump""" + name = ins.getOpcodeName() + rs, rt, rd, sa = (w >> 21) & 31, (w >> 16) & 31, (w >> 11) & 31, (w >> 6) & 31 + imm, uimm = S16(w), w & 0xFFFF + ft, fs, fd = rt, rd, sa + Rs, Rt = R(rs), R(rt) + L = lambda: "(u32)%s" % Rs + if name == "nop" or name in ("sync", "sync.l", "sync.p", "pref", "cache", "ei", "di", "mtc0", "mtsab", "mtsah"): return ";" + # ALU + if name in ("addu", "add"): return W(rd, "SE32((u32)%s + (u32)%s)" % (Rs, Rt)) + if name in ("subu", "sub"): return W(rd, "SE32((u32)%s - (u32)%s)" % (Rs, Rt)) + if name in ("addiu", "addi"): return W(rt, "SE32((u32)%s + %d)" % (Rs, imm)) + if name in ("daddu", "dadd"): return W(rd, "%s + %s" % (Rs, Rt)) + if name in ("dsubu", "dsub"): return W(rd, "%s - %s" % (Rs, Rt)) + if name in ("daddiu", "daddi"): return W(rt, "%s + (u64)(s64)%d" % (Rs, imm)) + if name == "and": return W(rd, "%s & %s" % (Rs, Rt)) + if name == "or": return W(rd, "%s | %s" % (Rs, Rt)) + if name == "xor": return W(rd, "%s ^ %s" % (Rs, Rt)) + if name == "nor": return W(rd, "~(%s | %s)" % (Rs, Rt)) + if name == "andi": return W(rt, "%s & 0x%X" % (Rs, uimm)) + if name == "ori": return W(rt, "%s | 0x%X" % (Rs, uimm)) + if name == "xori": return W(rt, "%s ^ 0x%X" % (Rs, uimm)) + if name == "lui": return W(rt, "SE32(0x%X)" % (uimm << 16)) + if name == "slt": return W(rd, "(s64)%s < (s64)%s" % (Rs, Rt)) + if name == "sltu": return W(rd, "%s < %s" % (Rs, Rt)) + if name == "slti": return W(rt, "(s64)%s < %d" % (Rs, imm)) + if name == "sltiu": return W(rt, "%s < (u64)(s64)%d" % (Rs, imm)) + if name == "sll": return W(rd, "SE32((u32)%s << %d)" % (Rt, sa)) + if name == "srl": return W(rd, "SE32((u32)%s >> %d)" % (Rt, sa)) + if name == "sra": return W(rd, "SE32((s32)(u32)%s >> %d)" % (Rt, sa)) + if name == "sllv": return W(rd, "SE32((u32)%s << (%s & 31))" % (Rt, Rs)) + if name == "srlv": return W(rd, "SE32((u32)%s >> (%s & 31))" % (Rt, Rs)) + if name == "srav": return W(rd, "SE32((s32)(u32)%s >> (%s & 31))" % (Rt, Rs)) + if name == "dsll": return W(rd, "%s << %d" % (Rt, sa)) + if name == "dsrl": return W(rd, "%s >> %d" % (Rt, sa)) + if name == "dsra": return W(rd, "(u64)((s64)%s >> %d)" % (Rt, sa)) + if name == "dsll32": return W(rd, "%s << %d" % (Rt, sa + 32)) + if name == "dsrl32": return W(rd, "%s >> %d" % (Rt, sa + 32)) + if name == "dsra32": return W(rd, "(u64)((s64)%s >> %d)" % (Rt, sa + 32)) + if name == "dsllv": return W(rd, "%s << (%s & 63)" % (Rt, Rs)) + if name == "dsrlv": return W(rd, "%s >> (%s & 63)" % (Rt, Rs)) + if name == "dsrav": return W(rd, "(u64)((s64)%s >> (%s & 63))" % (Rt, Rs)) + if name == "movz": return "if (%s == 0) { %s }" % (Rt, W(rd, Rs)) + if name == "movn": return "if (%s != 0) { %s }" % (Rt, W(rd, Rs)) + if name in ("mult", "multu", "mult1", "multu1", "madd", "maddu", "madd1", "maddu1"): + sgn = "u" not in name.replace("mult", "").replace("madd", "") + k = 1 if name.endswith("1") else 0 + prod = "(s64)(s32)(u32)%s * (s32)(u32)%s" % (Rs, Rt) if sgn else "(s64)((u64)(u32)%s * (u32)%s)" % (Rs, Rt) + acc = "((u64)c->hi.w[%d] << 32 | c->lo.w[%d]) + " % (2 * k, 2 * k) if name.startswith("madd") else "" + return "{ u64 p = %s(u64)(%s); c->lo.d[%d] = SE32((u32)p); c->hi.d[%d] = SE32((u32)(p >> 32)); %s }" % (acc, prod, k, k, W(rd, "c->lo.d[%d]" % k)) + if name in ("div", "divu", "div1", "divu1"): + k = 1 if name.endswith("1") else 0 + if name.startswith("divu"): + return "{ u32 a = (u32)%s, b = (u32)%s; if (b) { c->lo.d[%d] = SE32(a / b); c->hi.d[%d] = SE32(a %% b); } }" % (Rs, Rt, k, k) + return ("{ s32 a = (s32)(u32)%s, b = (s32)(u32)%s; if (b == -1 && a == (s32)0x80000000) { c->lo.d[%d] = SE32(a); c->hi.d[%d] = 0; } " + "else if (b) { c->lo.d[%d] = SE32(a / b); c->hi.d[%d] = SE32(a %% b); } else { c->lo.d[%d] = SE32(a < 0 ? 1 : -1); c->hi.d[%d] = SE32(a); } }") % (Rs, Rt, k, k, k, k, k, k) + if name == "mfhi": return W(rd, "c->hi.d[0]") + if name == "mflo": return W(rd, "c->lo.d[0]") + if name == "mfhi1": return W(rd, "c->hi.d[1]") + if name == "mflo1": return W(rd, "c->lo.d[1]") + if name == "mthi": return "c->hi.d[0] = %s;" % Rs + if name == "mtlo": return "c->lo.d[0] = %s;" % Rs + if name == "mthi1": return "c->hi.d[1] = %s;" % Rs + if name == "mtlo1": return "c->lo.d[1] = %s;" % Rs + if name == "mfsa": return W(rd, "c->sa") + if name == "mtsa": return "c->sa = (u32)%s;" % Rs + # loads / stores + ea = EA(w) + if name == "lb": return W(rt, "(u64)(s64)(s8)M8(%s)" % ea) + if name == "lbu": return W(rt, "M8(%s)" % ea) + if name == "lh": return W(rt, "(u64)(s64)(s16)M16(%s)" % ea) + if name == "lhu": return W(rt, "M16(%s)" % ea) + if name == "lw": return W(rt, "SE32(M32(%s))" % ea) + if name == "lwu": return W(rt, "M32(%s)" % ea) + if name == "ld": return W(rt, "M64(%s)" % ea) + if name == "lq": return "" if rt == 0 else "rc_lq(&c->r[%d], %s);" % (rt, ea) + if name == "sb": return "M8(%s) = (u8)%s;" % (ea, Rt) + if name == "sh": return "M16(%s) = (u16)%s;" % (ea, Rt) + if name == "sw": return "rc_sw(%s, (u32)%s);" % (ea, Rt) + if name == "sd": return "rc_sd(%s, %s);" % (ea, Rt) + if name == "sq": return ("{ R128 z = {{0,0}}; rc_sq(&z, %s); }" % ea) if rt == 0 else "rc_sq(&c->r[%d], %s);" % (rt, ea) + if name in ("lwl", "lwr", "ldl", "ldr"): return W(rt, "rc_%s(%s, %s)" % (name, Rt, ea)) + if name in ("swl", "swr", "sdl", "sdr"): return "rc_%s(%s, %s);" % (name, Rt, ea) + if name == "lwc1": return "c->f[%d] = MF(%s);" % (ft, ea) + if name == "swc1": return "MF(%s) = c->f[%d];" % (ea, ft) + if name == "lqc2": return "" if rt == 0 else "__builtin_memcpy(VF(%d), (void *)RC_QA(%s), 16);" % (rt, ea) + if name == "sqc2": return "__builtin_memcpy((void *)RC_QA(%s), VF(%d), 16);" % (ea, rt) + # FPU + F = lambda n: "c->f[%d]" % n + if name == "mfc1": return W(rt, "SE32(rc_fbits(c->f[%d]))" % fs) + if name == "mtc1": return "c->f[%d] = rc_bitsf((u32)%s);" % (fs, Rt) + if name == "cfc1": return W(rt, "SE32(c->fcc ? 0x00800000u : 0)") + if name == "ctc1": return "c->fcc = ((u32)%s >> 23) & 1;" % Rt + if name == "add.s": return "%s = rc_fc(%s + %s);" % (F(fd), F(fs), F(ft)) # rc_fc: no infinities on the EE + if name == "sub.s": return "%s = rc_fc(%s - %s);" % (F(fd), F(fs), F(ft)) + if name == "mul.s": return "%s = rc_fc(%s * %s);" % (F(fd), F(fs), F(ft)) + if name == "div.s": return "%s = rc_fc(rc_fdiv(%s, %s));" % (F(fd), F(fs), F(ft)) + if name == "sqrt.s": return "%s = rc_fsqrt(%s);" % (F(fd), F(ft)) + if name == "rsqrt.s": return "%s = rc_fdiv(%s, rc_fsqrt(%s));" % (F(fd), F(fs), F(ft)) + if name == "abs.s": return "%s = __builtin_fabsf(%s);" % (F(fd), F(fs)) + if name == "neg.s": return "%s = -%s;" % (F(fd), F(fs)) + if name == "mov.s": return "%s = %s;" % (F(fd), F(fs)) + if name == "max.s": return "%s = fmaxf(%s, %s);" % (F(fd), F(fs), F(ft)) + if name == "min.s": return "%s = fminf(%s, %s);" % (F(fd), F(fs), F(ft)) + if name == "adda.s": return "c->facc = rc_fc(%s + %s);" % (F(fs), F(ft)) + if name == "suba.s": return "c->facc = %s - %s;" % (F(fs), F(ft)) + if name == "mula.s": return "c->facc = %s * %s;" % (F(fs), F(ft)) + if name == "madda.s": return "c->facc = rc_fc(c->facc + rc_fc(%s * %s));" % (F(fs), F(ft)) + if name == "msuba.s": return "c->facc = rc_fc(c->facc - rc_fc(%s * %s));" % (F(fs), F(ft)) + if name == "madd.s": return "%s = rc_fc(c->facc + rc_fc(%s * %s));" % (F(fd), F(fs), F(ft)) + if name == "msub.s": return "%s = rc_fc(c->facc - rc_fc(%s * %s));" % (F(fd), F(fs), F(ft)) + if name == "cvt.s.w": return "%s = (f32)(s32)rc_fbits(%s);" % (F(fd), F(fs)) + if name == "cvt.w.s": return "%s = rc_bitsf((u32)rc_cvtws(%s));" % (F(fd), F(fs)) + if name == "c.f.s": return "c->fcc = 0;" + if name == "c.eq.s": return "c->fcc = %s == %s;" % (F(fs), F(ft)) + if name == "c.lt.s": return "c->fcc = %s < %s;" % (F(fs), F(ft)) + if name == "c.le.s": return "c->fcc = %s <= %s;" % (F(fs), F(ft)) + # COP2 transfers + if name in ("qmfc2", "qmfc2.ni", "qmfc2.i"): return ("rc_vu0_resume(); " if name.endswith(".i") else "") + ("" if rt == 0 else "__builtin_memcpy(&c->r[%d], VF(%d), 16);" % (rt, rd)) + if name in ("qmtc2", "qmtc2.ni", "qmtc2.i"): return "" if rd == 0 else "__builtin_memcpy(VF(%d), &c->r[%d], 16);" % (rd, rt) + if name in ("cfc2", "cfc2.ni", "cfc2.i"): + src = {16: "rc_vu0.status", 17: "rc_vu0.mac", 18: "rc_vu0.clip", 20: "rc_fbits(rc_vu0.r)", 21: "rc_fbits(rc_vu0.i)", 22: "rc_fbits(rc_vu0.q)", + 29: "0"}.get(rd, "rc_vu0.vi[%d]" % (rd & 15) if rd < 16 else "0") + return W(rt, "SE32(%s)" % src) + if name in ("ctc2", "ctc2.ni", "ctc2.i"): + if rd < 16: return "" if rd == 0 else "rc_vu0.vi[%d] = (u32)%s & 0xFFFF; rc_vu0_resume();" % (rd, Rt) + dst = {18: "rc_vu0.clip = (u32)%s & 0xFFFFFF;", 20: "rc_vu0.r = rc_bitsf((u32)%s);", 21: "rc_vu0.i = rc_bitsf((u32)%s);", + 22: "rc_vu0.q = rc_bitsf((u32)%s);", 16: "rc_vu0.status = (u32)%s;", 27: "rc_vu0.cmsar0 = (u32)%s & 0xFFFF;", + 31: "vu_run(1, ((u32)%s & 0xFFFF) * 8);"}.get(rd) # CMSAR1: starts VU1 there + return (dst % Rt) if dst else ";" + if name == "mfc0": + return W(rt, "SE32(rc_cop0(%d))" % rd) + if name == "syscall": return "rc_syscall(c);" + if name in ("break", "teq", "tne", "tge", "tlt", "teqi", "tnei"): + if name == "teq": return "if (%s == %s) rc_break(0x%08X);" % (Rs, Rt, pc) + if name == "tne": return "if (%s != %s) rc_break(0x%08X);" % (Rs, Rt, pc) + return "rc_break(0x%08X);" % pc + if name.startswith("v"): return vu_op(name, w, pc) + if name.startswith("p") or name.startswith("pm"): return mmi_op(name, w, pc) + warn(name, pc) + return 'rc_bad(0x%08X, "%s");' % (pc, name) + + +def branch_cond(name, w): + rs, rt = (w >> 21) & 31, (w >> 16) & 31 + Rs, Rt = R(rs), R(rt) + base = name[:-1] if name.endswith("l") and name not in ("bal",) and name[:-1] in ( + "beq", "bne", "blez", "bgtz", "bltz", "bgez", "bltzal", "bgezal", "bc1f", "bc1t", "bc0f", "bc0t", "bc2f", "bc2t") else name + return { + "beq": "%s == %s" % (Rs, Rt), "bne": "%s != %s" % (Rs, Rt), + "blez": "(s64)%s <= 0" % Rs, "bgtz": "(s64)%s > 0" % Rs, + "bltz": "(s64)%s < 0" % Rs, "bgez": "(s64)%s >= 0" % Rs, + "bltzal": "(s64)%s < 0" % Rs, "bgezal": "(s64)%s >= 0" % Rs, + "bc1f": "!c->fcc", "bc1t": "c->fcc", + "bc0f": "0", "bc0t": "1", # CPCOND0 (DMA done): always done on the host + "bc2f": "0", "bc2t": "0", + }.get(base), name != base + + +def call_target(addr): + """C statement that calls the function at addr with the shared context""" + if addr in rc_addr: return P + "%s(c);" % rc_addr[addr] + if addr in c_funcs: return P + "thunk_%s(c);" % c_funcs[addr] + if addr in core_rc: return "%s(c);" % core_rc[addr] + return "rc_dispatch(c, 0x%08X);" % addr + + +thunks_needed = set() + + +NEED_VU0_FLAGS = True +def translate(name, rng=None, entries=None): + va, size = rng if rng else fsize[name] + n = size // 4 + ins = [(va + 4 * i, word(va + 4 * i)) for i in range(n + 1)] # + the word after: a last delay slot + dec = {pc: rabbitizer.Instruction(w, vram=pc, category=rabbitizer.InstrCategory.R5900) for pc, w in ins} + ins_all, ins = ins, ins[:n] + # the COP2 MAC/status flags cost a computation per FMAC op: only functions + # that read them (cfc2 from $vi16 or $vi17) get them + global NEED_VU0_FLAGS + NEED_VU0_FLAGS = any(dec[pc].getOpcodeName().startswith("cfc2") and ((w >> 11) & 31) in (16, 17) for pc, w in ins) + labels = set() + jr_reg = False + for pc, w in ins: + d = dec[pc] + nm = d.getOpcodeName() + if d.isBranch() or nm in ("j",): + t = d.getBranchVramGeneric() if d.isBranch() else ((pc + 4) & 0xF0000000) | ((w & 0x3FFFFFF) << 2) + if va <= t < va + size: labels.add(t) + if nm == "jr" and ((w >> 21) & 31) != 31: jr_reg = True + if jr_reg: + labels = {pc for pc, w in ins} + if entries: + labels |= set(entries) + out = ["static void %sregion_%s(RcCtx *c, u32 entry)\n{" % (P, name), + " switch (entry) { %s }" % " ".join("case 0x%08X: goto L_%08X;" % (e, e) for e in sorted(entries))] + else: + out = ["void %s%s(RcCtx *c)\n{" % (P, name)] + if name in TRACE_FUNCS: # testing: UYA_RC_TRACE at build time (rc_trace logs the calls) + out.append(' { extern void rc_trace(RcCtx *, const char *); rc_trace(c, "%s"); }' % name) + if name == "func_001F7EF8": # testing: drawquad's GS words checked (UYA_QUADCHECK) + out.append(" { extern void uya_dbg_quad(RcCtx *); uya_dbg_quad(c); }") + if jr_reg: + # the return address on entry: `jr $t6` after `move $t6, $ra` (func_L00_001FE688) + # is a return, not a jump to the caller's code as a function of its own + out.append(" u32 rc_ra0 = (u32)c->r[31].d[0];") + i = 0 + while i < n: + pc, w = ins[i] + d = dec[pc] + nm = d.getOpcodeName() + if pc in labels: out.append("L_%08X:;" % pc) + has_delay = d.hasDelaySlot() and i + 1 <= n + delay = "" + if has_delay: + dpc, dw = ins_all[i + 1] + delay = simple(dec[dpc], dw, dpc) or ";" + if nm in ("jr",): + rs = (w >> 21) & 31 + if rs == 31: + out.append(" %s return;" % delay) + else: + out.append(" { u32 tgt = (u32)%s; %s if (tgt == rc_ra0) return; switch (tgt) { @CASES@ default: rc_dispatch(c, tgt); return; } }" % (R(rs), delay)) + if has_delay and pc + 4 in labels: out.append(" if (0) { L_%08X:; %s }" % (pc + 4, delay)) + i += 2; continue + if nm == "jalr": + rs, rd = (w >> 21) & 31, (w >> 11) & 31 + out.append(" { u32 tgt = (u32)%s; %s %s rc_dispatch(c, tgt); }" % (R(rs), W(rd, "0x%08XULL" % (pc + 8)), delay)) + if has_delay and pc + 4 in labels: out.append(" if (0) { L_%08X:; %s }" % (pc + 4, delay)) + i += 2; continue + if nm in ("jal", "j"): + t = ((pc + 4) & 0xF0000000) | ((w & 0x3FFFFFF) << 2) + if nm == "jal": + out.append(" c->r[31].d[0] = 0x%08XULL; %s %s" % (pc + 8, delay, call_target(t))) + elif va <= t < va + size: + out.append(" %s goto L_%08X;" % (delay, t)) + else: + out.append(" %s %s return;" % (delay, call_target(t))) + if has_delay and pc + 4 in labels: out.append(" if (0) { L_%08X:; %s }" % (pc + 4, delay)) + i += 2; continue + if d.isBranch(): + cond, likely = branch_cond(nm, w) + t = d.getBranchVramGeneric() + link = nm in ("bltzal", "bgezal", "bltzall", "bgezall") + lk = "c->r[31].d[0] = 0x%08XULL; " % (pc + 8) if link else "" + if cond is None: + warn(nm, pc); out.append(' rc_bad(0x%08X, "%s");' % (pc, nm)); i += 1; continue + inside = va <= t < va + size + go = ("goto L_%08X;" % t) if inside and not link else ("%s return;" % call_target(t) if not link else call_target(t)) + if likely: + out.append(" if (%s) { %s%s %s }" % (cond, lk, delay, go)) + else: + out.append(" { int bt = %s; %s%s if (bt) { %s } }" % (cond, lk, delay, go)) + if has_delay and pc + 4 in labels: out.append(" if (0) { L_%08X:; %s }" % (pc + 4, delay)) + i += 2; continue + out.append(" " + (simple(d, w, pc) or ";")) + i += 1 + # the last instruction does not leave: it runs on into the next function + last = n - 2 if n >= 2 and dec[ins[n - 2][0]].hasDelaySlot() else n - 1 + lnm = dec[ins[last][0]].getOpcodeName() if n else "jr" + lw = ins[last][1] if n else 0 + if LEVEL is not None and not (lnm in ("j", "jr") or (lnm == "beq" and (lw >> 16) & 0x3FF == 0) or lnm in ("b", "teq", "break")): + out.append(" %s return;" % call_target(va + size)) + out.append("}\n") + txt = "\n".join(out) + emitted = set(int(x, 16) for x in re.findall(r"\bL_([0-9A-F]{8}):;", txt)) # also the delay-slot labels inside `if (0) { ... }` + txt = txt.replace("@CASES@", " ".join("case 0x%08X: goto L_%08X;" % (l, l) for l in sorted(emitted))) + for t in set(int(x, 16) for x in re.findall(r"goto L_([0-9A-F]{8});", txt)) - emitted: + warn("branch into a delay slot", t) + txt = txt.replace("goto L_%08X;" % t, 'rc_bad(0x%08X, "branch into a delay slot"); return;' % t) + return txt + + +# ---- emit ------------------------------------------------------------------- +cls_c = {"i": "s32", "l": "s64", "f": "f32", "d": "double", "q": "s32", "s": "s32", "v": "void"} + + +def arg_regs(params): + """(gpr or fpr expression) per parameter, EE argument order""" + gi, fi, res = 4, 12, [] + for p in params: + if p == "f": + res.append(("f", fi)); fi += 1 + else: + res.append(("g", gi)); gi += 1 + return res + + +# ---- regions: hand-written functions that jump into each other's middles are one routine +def jump_targets(nm): + va, size = fsize[nm] + res = [] + for i in range(size // 4): + pc = va + 4 * i; w = word(pc) + d = rabbitizer.Instruction(w, vram=pc, category=rabbitizer.InstrCategory.R5900) + if d.getOpcodeName() == "j": res.append(((pc + 4) & 0xF0000000) | ((w & 0x3FFFFFF) << 2)) + elif d.isBranch() and not d.doesLink(): res.append(d.getBranchVramGeneric()) + return res +by_start = sorted((fsize[n][0], fsize[n][0] + fsize[n][1], n) for n in todo) +def owner(a): + for lo, hi, n in by_start: + if lo <= a < hi: return n + return None +parent = {n: n for n in todo} +def find(n): + while parent[n] != n: parent[n] = parent[parent[n]]; n = parent[n] + return n +for n in todo: + for t in jump_targets(n): + o = owner(t) + if o and o != n and t != fsize[o][0]: + parent[find(o)] = find(n) +groups = {} +for n in todo: groups.setdefault(find(n), []).append(n) +region_of = {} +spans = sorted((min(fsize[n][0] for n in g), max(fsize[n][0] + fsize[n][1] for n in g)) for g in groups.values() if len(g) > 1) +merged = [] # overlapping regions are one region (a function falling through into the next group) +for lo, hi in spans: + if merged and lo < merged[-1][1]: merged[-1][1] = max(merged[-1][1], hi) + else: merged.append([lo, hi]) +for lo, hi in merged: + # everything in between belongs to the region too + mem = [n for n in todo if lo <= fsize[n][0] < hi] + hi = max([hi] + [fsize[n][0] + fsize[n][1] for n in mem]) + for n in mem: region_of[n] = (lo, hi, sorted(mem, key=lambda x: fsize[x][0])) + +hdr = ['/* generated by tools/recomp.py */\n#include "rc.h"\nstatic inline double rc_d(u64 v) { double d; __builtin_memcpy(&d, &v, 8); return d; }\nu32 rc_cop0(int r);'] +for nm in todo: + hdr.append("void %s%s(RcCtx *c);" % (P, nm)) +for f in sorted(set(core_rc.values())): + hdr.append("void %s(RcCtx *c);" % f) +thunk_text = ['#include "rc_decls.h"'] +_seen_thunk = set() +for a, cn in sorted(c_funcs.items()): + if cn in _seen_thunk: continue # one C function at several places + _seen_thunk.add(cn) + s = sigs[cn] + ps = s["params"] + regs = arg_regs(ps) + if s["var"]: decl_args = (", ".join(cls_c[p] for p in ps) + ", ...") if ps else "..." + else: decl_args = ", ".join(cls_c[p] for p in ps) if ps else "void" + call_args = [] + for p, (k, n_) in zip(ps, regs): + if k == "f": call_args.append("c->f[%d]" % n_) + elif n_ > 11 and p == "l": call_args.append("(s64)M64((u32)c->r[29].d[0] + %d)" % (8 * (n_ - 12))) # a 64-bit stack slot + elif n_ > 11: call_args.append("(%s)M32((u32)c->r[29].d[0] + %d)" % (cls_c[p], 8 * (n_ - 12))) + elif p == "l": call_args.append("(s64)c->r[%d].d[0]" % n_) + elif p == "d": call_args.append("rc_d(c->r[%d].d[0])" % n_) + else: call_args.append("(s32)c->r[%d].d[0]" % n_) + if s["var"]: + gi = 4 + sum(1 for p in ps if p != "f") + call_args += ["(s32)c->r[%d].d[0]" % g for g in range(gi, 12)] + ret = s["ret"] + hdr.append("void %sthunk_%s(RcCtx *c);" % (P, cn)) + thunk_text.append("extern %s %s(%s);" % (cls_c[ret], cn, decl_args)) + body = "%s(%s)" % (cn, ", ".join(call_args)) + if ret == "v": store = "%s;" % body + elif ret == "f": store = "c->f[0] = %s;" % body + elif ret == "l": store = "c->r[2].d[0] = (u64)%s;" % body + elif ret == "d": store = "{ double v_ = %s; __builtin_memcpy(&c->r[2].d[0], &v_, 8); }" % body + else: store = "c->r[2].d[0] = SE32(%s);" % body + thunk_text.append("void %sthunk_%s(RcCtx *c) { u32 sp0 = rc_sp; rc_sp = (u32)c->r[29].d[0] & ~15u; %s rc_sp = sp0; }" % (P, cn, store)) +open(os.path.join(GEN, "rc_decls.h"), "w").write("\n".join(hdr) + "\n") +open(os.path.join(GEN, "rc_thunks.c"), "w").write("\n".join(thunk_text) + "\n") +for old in [f for f in os.listdir(GEN) if re.fullmatch(r"rc_funcs_\d+\.c", f)]: + os.remove(os.path.join(GEN, old)) +CH = 20 +for k in range(0, len(todo), CH): + txt = ['/* generated by tools/recomp.py from the user\'s own %s: do not edit */\n#include "rc_decls.h"\n' % ("executable" if LEVEL is None else "disc (level %s's program)" % LV)] + for nm in todo[k:k + CH]: + if nm in region_of: + lo, hi, mem = region_of[nm] + if nm == mem[0]: + txt.append(translate(nm, (lo, hi - lo), [fsize[m][0] for m in mem])) + for m in mem: + txt.append("void %s%s(RcCtx *c) { %sregion_%s(c, 0x%08X); }" % (P, m, P, nm, fsize[m][0])) + continue + txt.append(translate(nm)) + open(os.path.join(GEN, "rc_funcs_%03d.c" % (k // CH)), "w").write("\n".join(txt)) + +# wrappers: the C entry for each translated function, under its own name +wr = ['/* generated by tools/recomp.py: C-callable entries for the translated functions */\n#include "rc.h"\n'] +for nm in todo: + s = sigs.get(base_of.get(nm, nm)) + wr.append("void %s%s(RcCtx *c);" % (P, nm)) + if s and s.get("src") == "decl" and s["params"] is not None and not s.get("unk"): + ps, ret = s["params"], s["ret"] + else: + ps, ret = ["i"] * 8, "l" + regs = arg_regs(ps) + plist = ", ".join("%s a%d" % (cls_c[p], k) for k, p in enumerate(ps)) or "void" + sets = [] + for k, (p, (kind, n_)) in enumerate(zip(ps, regs)): + if kind == "f": sets.append("c.f[%d] = a%d;" % (n_, k)) + elif n_ > 11 and p == "l": sets.append("M64((u32)c.r[29].d[0] + %d) = (u64)a%d;" % (8 * (n_ - 12), k)) + elif n_ > 11: sets.append("M32((u32)c.r[29].d[0] + %d) = (u32)a%d;" % (8 * (n_ - 12), k)) + elif p == "l": sets.append("c.r[%d].d[0] = (u64)a%d;" % (n_, k)) + elif p == "d": sets.append("__builtin_memcpy(&c.r[%d].d[0], &a%d, 8);" % (n_, k)) + else: sets.append("c.r[%d].d[0] = SE32(a%d);" % (n_, k)) + if ret == "v": rr = "" + elif ret == "f": rr = "return c.f[0];" + elif ret == "l": rr = "return (s64)c.r[2].d[0];" + elif ret == "d": rr = "{ double v_; __builtin_memcpy(&v_, &c.r[2].d[0], 8); return v_; }" + else: rr = "return (s32)c.r[2].d[0];" + wr.append("%s %s(%s) { RcCtx c; rc_enter(&c); %s %s%s(&c); %s }" % ( + cls_c[ret], nm, plist, " ".join(sets), P, nm, rr)) +open(os.path.join(GEN, "rc_wrappers.c"), "w").write("\n".join(wr) + "\n") + +# dispatch table: every translated function and every thunk, by address +tb = ['/* generated by tools/recomp.py */\n#include "rc.h"\ntypedef void (*rc_fn)(RcCtx *);'] +rows = [] +for a, nm in sorted(rc_addr.items()): + rows.append((a, P + nm)) +for a, cn in sorted(c_funcs.items()): + if a not in rc_addr: rows.append((a, P + "thunk_" + cn)) +have_rows = {a for a, _ in rows} +for a, f in sorted(core_rc.items()): + if a not in have_rows: rows.append((a, f)) +rows.sort() +for a, f in rows: tb.append("void %s(RcCtx *c);" % f) +tb.append("const struct { u32 addr; rc_fn fn; } %stable[] = {" % P) +for a, f in rows: tb.append(" {0x%08X, %s}," % (a, f)) +tb.append("};\nconst int %stable_count = %d;" % (P, len(rows))) +open(os.path.join(GEN, "rc_table.c"), "w").write("\n".join(tb) + "\n") + +# asm_stubs.c without the translated functions +p = os.path.join(GEN, "asm_stubs.c") +if os.path.exists(p): + keep = [l for l in open(p) if not any(("long long %s(void)" % nm) in l for nm in todo)] + if LEVEL is not None: + tset = set(todo) + keep = [l for l in open(p) if not (re.match(r"long long (\w+)\(void\)", l) and re.match(r"long long (\w+)\(void\)", l).group(1) in tset)] + open(p, "w").write("".join(keep)) +print("recompiled %d functions (%d instructions); %d C thunks; unsupported instructions: %s" % ( + len(todo), sum(fsize[n][1] // 4 for n in todo), len(c_funcs), + ", ".join("%s x%d" % (k, len(v)) for k, v in sorted(warnings.items())) or "none")) + +# functions translated here that the generator did not know as defined (the +# extra library ranges): no absolute symbol for them, and a trampoline each +known = set(entries) +extra = [n for n in todo if n not in known] +if LEVEL is not None: + # the level's trampolines: its C functions (gen_host.py) and every translated one + p = os.path.join(GEN, "tramp_table.c") + t = open(p).read() + rows = {int(a, 16): n for a, n in re.findall(r"\{0x([0-9A-F]{8}), \(void \*\)(\w+)\},", t)} + for n in todo: rows[fsize[n][0]] = n + rows.update(_host_rows) + TT = "uya_tramp_table_l%s" % LV + t = '/* generated by gen_host.py and recomp.py: level %s, PS2 address -> host function */\n#include "uya_runtime.h"\n' % LV + t += "".join("extern void %s(void);\n" % n for n in sorted(set(rows.values()))) + t += "const uya_tramp_t %s[] = {\n" % TT + "".join(" {0x%08X, (void *)%s},\n" % r for r in sorted(rows.items())) + t += "};\nconst int uya_tramp_count_l%s = %d;\n" % (LV, len(rows)) + open(p, "w").write(t) + extra = [] +if extra: + p = os.path.join(GEN, "abs_syms.s") + lines = open(p).read().split("\n") + drop = set(extra) | {"_" + n for n in extra} + out, skip = [], False + for l in lines: + q = re.match(r"\.(globl|set)\s+(\w+)", l) + if q and q.group(2) in drop: continue + out.append(l) + open(p, "w").write("\n".join(out)) + p = os.path.join(GEN, "tramp_table.c") + t = open(p).read() + rows = re.findall(r"\{0x([0-9A-F]{8}), \(void \*\)(\w+)\},", t) + have = {int(a, 16) for a, _ in rows} + rows += [("%08X" % fsize[n][0], n) for n in extra if fsize[n][0] not in have] + rows.sort() + decls = sorted({n for _, n in rows}) + t = '/* generated by gen_host.py and recomp.py: PS2 address -> host function */\n#include "uya_runtime.h"\n' + t += "".join("extern void %s(void);\n" % n for n in decls) + t += "const uya_tramp_t uya_tramp_table[] = {\n" + "".join(" {0x%s, (void *)%s},\n" % r for r in rows) + t += "};\nconst int uya_tramp_count = %d;\n" % len(rows) + open(p, "w").write(t) diff --git a/games/rac1/tools/source_patches.py b/games/rac1/tools/source_patches.py new file mode 100644 index 0000000..dcb2013 --- /dev/null +++ b/games/rac1/tools/source_patches.py @@ -0,0 +1,59 @@ +# Small text fixes applied to the decomp's C before it is compiled for the PC. +# Each is (file name, exact old text, new text). They are for places where the +# C is right for the EE calling convention but not for x86. +PATCHES = [ + # func_003894A0(colour a, colour b, float t): on the EE the float travels in + # $f12 whatever its position, so these files declare it first. On x86 the + # order matters; the macro puts the arguments back in definition order. + ("3A5478.c", "extern s32 func_003894A0(f32, s32, s32);", + "extern s32 func_003894A0(s32, s32, f32);\n#define func_003894A0(f, a, b) func_003894A0((a), (b), (f))"), + ("3B5BF8.c", "extern s32 func_003894A0(f32, s32, s32);", + "extern s32 func_003894A0(s32, s32, f32);\n#define func_003894A0(f, a, b) func_003894A0((a), (b), (f))"), + # func_0038C3C0(a, unused, c, f) calls func_00389D18(str, len, scale) with + # only the float: on the EE, str and len are still a and unused in $a0/$a1. + ("3896E8.c", "t = ((s32 (*)(f32))func_00389D18)(1.0f);", + "t = ((s32 (*)(u8 *, s32, f32))func_00389D18)((u8 *)a, unused, 1.0f);"), + # func_0038C9B8's second and seventh arguments are 64-bit on the EE (one + # register each) and are passed on as such to func_0038B1E8; its only + # caller (3E5378.c) passes them as 64-bit values. + ("3896E8.c", "void func_0038C9B8(s32 a, s32 b, s32 c, s32 d, s32 e, s32 g, s32 h, f32 x) {", + "void func_0038C9B8(s32 a, long b, s32 c, s32 d, s32 e, s32 g, unsigned long h, f32 x) {"), + # RAC1. func_L00_001FE940(u128 *v, ...) copies *v with a 16-byte load; its + # caller (func_L00_002A7078) passes a float[4] member of a local union, which + # the EE stack frame keeps 16-byte aligned and the x86 one does not: copy it + # as four words (the pointer goes through an integer so that the compiler + # does not assume the vector type's alignment for it). + ("drawquad_001FD1D8.c", " *tp = *v;\n", + " { const unsigned int *s_ = (const unsigned int *)(unsigned int)v; unsigned int *d_ = (unsigned int *)(unsigned int)tp;\n" + " d_[0] = s_[0]; d_[1] = s_[1]; d_[2] = s_[2]; d_[3] = s_[3]; }\n"), + # func_L00_002EABD0 (a camera rig's aim) divides by distances that are 0 + # on the first frame; the EE gives +-MAX there and FastArcSin a finite + # angle, x86 gives inf/NaN and the camera's matrices stay NaN (nothing of + # Veldin is drawn once the gameplay camera takes over) + ("vendor_002E1660.c", " y = func_001F9FC0(e1 / x);\n", " y = func_001F9FC0(ee_fdiv(e1, x));\n"), + ("vendor_002E1660.c", " r = func_001F9FC0(h / bc);\n", " r = func_001F9FC0(ee_fdiv(h, bc));\n"), + ("vendor_002E1660.c", " r = 1.5707964f - func_001F9FC0(e / x);\n", " r = 1.5707964f - func_001F9FC0(ee_fdiv(e, x));\n"), + # func_L00_001EB6A8 is void in the C but ends with FastAddRots(a, *p), whose + # result stays in $f0 on the EE; every caller (the camera rigs) uses it as + # the function's float result. On x86 a void function returns nothing (the + # callers read an empty x87 register: NaN in the camera's matrices). + ("camera_001EB508.c", "void func_L00_001EB6A8(float *p, float a, float b, float c, float d, float lim) {", + "float func_L00_001EB6A8(float *p, float a, float b, float c, float d, float lim) {"), + ("camera_001EB508.c", " *p = -func_001F9B88(r);\n }\n func_001FA748(a, *p);\n}", + " *p = -func_001F9B88(r);\n }\n return func_001FA748(a, *p);\n}"), + # func_001F7EF8 (drawquad, hand-written) reads its caller's quad as one + # block: 4 positions at +0, the colours at +64, the UVs at +80 and the GS + # words at +112 -- on the EE these callers' locals sit in that order in the + # frame. On x86 separate locals are laid out as the compiler likes, so they + # are put in one struct here. + ("draw.c", " FRow m[4];\n int colors[4];\n float uv[4][2];\n unsigned long pkt[4];\n", + " struct { FRow m[4]; int colors[4]; float uv[4][2]; unsigned long pkt[4]; } q4c30_;\n" + "#define m q4c30_.m\n#define colors q4c30_.colors\n#define uv q4c30_.uv\n#define pkt q4c30_.pkt\n"), + ("draw.c", " func_001F7EF8(m, 0, 0);\n }\n}\n", + " func_001F7EF8(m, 0, 0);\n }\n}\n#undef m\n#undef colors\n#undef uv\n#undef pkt\n"), + ("space.c", " Vec4 quad_positions[4];\n int colors[4];\n float texture_coordinates[4][2];\n u64 quad_state[4];\n", + " struct { Vec4 quad_positions[4]; int colors[4]; float texture_coordinates[4][2]; u64 quad_state[4]; } q33ab8_;\n" + "#define quad_positions q33ab8_.quad_positions\n#define colors q33ab8_.colors\n#define texture_coordinates q33ab8_.texture_coordinates\n#define quad_state q33ab8_.quad_state\n"), + ("space.c", " func_001F7EF8(quad_positions, 0, 0);\n }\n}\n", + " func_001F7EF8(quad_positions, 0, 0);\n }\n}\n#undef quad_positions\n#undef colors\n#undef texture_coordinates\n#undef quad_state\n"), +] diff --git a/games/rac1/tools/vu_isa.py b/games/rac1/tools/vu_isa.py new file mode 100644 index 0000000..d3acfd5 --- /dev/null +++ b/games/rac1/tools/vu_isa.py @@ -0,0 +1,148 @@ +"""vu_isa.py: decoding of PS2 VU micro-mode instructions (shared by vu_recomp.py). + +A VU micro instruction is a 64-bit pair: the lower word (at the lower address) +holds the lower-unit op (load/store, integer, branch, DIV/EFU...), the upper +word the FMAC op plus the I/E/M/D/T bits. With the I bit set the lower word is +a float immediate for the I register (LOI). +""" + +XYZW = "xyzw" + + +def dest(w): + d = (w >> 21) & 15 + return [i for i in range(4) if d & (8 >> i)] + + +def dstr(w): + return "".join(XYZW[i] for i in dest(w)) + + +class Up: + """decoded upper op: name, kind, fd/fs/ft, comps, bc ('x'..'w', 'q', 'i' or None), acc (writes ACC)""" + def __init__(self, name, w, **kw): + self.name, self.w = name, w + self.comps = dest(w) + self.ft, self.fs, self.fd = (w >> 16) & 31, (w >> 11) & 31, (w >> 6) & 31 + self.bc = None + self.__dict__.update(kw) + + def text(self): + return "%s.%s ft=%d fs=%d fd=%d%s" % (self.name, dstr(self.w), self.ft, self.fs, self.fd, + (" bc=" + self.bc) if self.bc else "") + + +_ARITH1 = {0x00: "add", 0x04: "sub", 0x08: "madd", 0x0C: "msub", 0x10: "max", 0x14: "mini", 0x18: "mul"} +_S1 = {0x1C: ("mul", "q"), 0x1D: ("max", "i"), 0x1E: ("mul", "i"), 0x1F: ("mini", "i"), + 0x20: ("add", "q"), 0x21: ("madd", "q"), 0x22: ("add", "i"), 0x23: ("madd", "i"), + 0x24: ("sub", "q"), 0x25: ("msub", "q"), 0x26: ("sub", "i"), 0x27: ("msub", "i"), + 0x28: ("add", None), 0x29: ("madd", None), 0x2A: ("mul", None), 0x2B: ("max", None), + 0x2C: ("sub", None), 0x2D: ("msub", None), 0x2F: ("mini", None)} +_ARITH2 = {0x00: "add", 0x04: "sub", 0x08: "madd", 0x0C: "msub", 0x18: "mul"} +_S2 = {0x1C: ("mul", "q"), 0x20: ("add", "q"), 0x21: ("madd", "q"), 0x22: ("add", "i"), 0x23: ("madd", "i"), + 0x24: ("sub", "q"), 0x25: ("msub", "q"), 0x26: ("sub", "i"), 0x27: ("msub", "i"), 0x1E: ("mul", "i"), + 0x28: ("add", None), 0x29: ("madd", None), 0x2A: ("mul", None), 0x2C: ("sub", None), 0x2D: ("msub", None)} + + +def decode_upper(w): + op = w & 0x3F + if op < 0x3C: + if op < 0x1C: + base = op & 0x3C + if base in _ARITH1: + return Up(_ARITH1[base], w, kind="arith", bc=XYZW[op & 3], acc=False) + if op in _S1: + n, bc = _S1[op] + return Up(n, w, kind="arith", bc=bc, acc=False) + if op == 0x2E: + return Up("opmsub", w, kind="opmsub", acc=False) + return Up("?up%02X" % op, w, kind="bad") + op2 = ((w >> 6) & 31) << 2 | (w & 3) + if op2 < 0x1C and (op2 & 0x3C) in _ARITH2 and op2 not in range(0x10, 0x18): + return Up(_ARITH2[op2 & 0x3C] + "a", w, kind="arith", bc=XYZW[op2 & 3], acc=True, base=_ARITH2[op2 & 0x3C]) + if op2 in _S2: + n, bc = _S2[op2] + return Up(n + "a", w, kind="arith", bc=bc, acc=True, base=n) + if 0x10 <= op2 <= 0x13: + return Up("itof%d" % (0, 4, 12, 15)[op2 & 3], w, kind="itof", shift=(0, 4, 12, 15)[op2 & 3]) + if 0x14 <= op2 <= 0x17: + return Up("ftoi%d" % (0, 4, 12, 15)[op2 & 3], w, kind="ftoi", shift=(0, 4, 12, 15)[op2 & 3]) + if op2 == 0x1D: return Up("abs", w, kind="abs") + if op2 == 0x1F: return Up("clipw", w, kind="clip") + if op2 == 0x2E: return Up("opmula", w, kind="opmula") + if op2 == 0x2F: return Up("nop", w, kind="nop") + return Up("?up2_%02X" % op2, w, kind="bad") + + +class Lo: + def __init__(self, name, w, **kw): + self.name, self.w = name, w + self.comps = dest(w) + self.it, self.is_, self.id_ = (w >> 16) & 31, (w >> 11) & 31, (w >> 6) & 31 + self.ft, self.fs, self.fd = self.it, self.is_, self.id_ + self.fsf, self.ftf = (w >> 21) & 3, (w >> 23) & 3 + self.__dict__.update(kw) + + def text(self): + return "%s.%s it=%d is=%d id=%d %s" % (self.name, dstr(self.w), self.it, self.is_, self.id_, + " ".join("%s=%s" % (k, getattr(self, k)) for k in ("imm",) if hasattr(self, k))) + + +def s11(w): + v = w & 0x7FF + return v - 0x800 if v & 0x400 else v + + +_LOWER_S2 = {0x30: "move", 0x31: "mr32", 0x34: "lqi", 0x35: "sqi", 0x36: "lqd", 0x37: "sqd", 0x38: "div", + 0x39: "sqrt", 0x3A: "rsqrt", 0x3B: "waitq", 0x3C: "mtir", 0x3D: "mfir", 0x3E: "ilwr", 0x3F: "iswr", + 0x40: "rnext", 0x41: "rget", 0x42: "rinit", 0x43: "rxor", 0x64: "mfp", 0x68: "xtop", 0x69: "xitop", + 0x6C: "xgkick", 0x70: "esadd", 0x71: "ersadd", 0x72: "eleng", 0x73: "erleng", 0x74: "eatanxy", + 0x75: "eatanxz", 0x76: "esum", 0x78: "esqrt", 0x79: "ersqrt", 0x7A: "ercpr", 0x7B: "waitp", + 0x7C: "esin", 0x7D: "eatan", 0x7E: "eexp"} +_LOWER = {0x00: "lq", 0x01: "sq", 0x04: "ilw", 0x05: "isw", 0x08: "iaddiu", 0x09: "isubiu", 0x10: "fceq", + 0x11: "fcset", 0x12: "fcand", 0x13: "fcor", 0x14: "fseq", 0x15: "fsset", 0x16: "fsand", 0x17: "fsor", + 0x18: "fmeq", 0x1A: "fmand", 0x1B: "fmor", 0x1C: "fcget", 0x20: "b", 0x21: "bal", 0x24: "jr", + 0x25: "jalr", 0x28: "ibeq", 0x29: "ibne", 0x2C: "ibltz", 0x2D: "ibgtz", 0x2E: "iblez", 0x2F: "ibgez"} +BRANCHES = {"b", "bal", "jr", "jalr", "ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez"} + + +def decode_lower(w): + op = w >> 25 + if op == 0x40: + f = w & 0x3F + if f < 0x3C: + n = {0x30: "iadd", 0x31: "isub", 0x32: "iaddi", 0x34: "iand", 0x35: "ior"}.get(f) + if n is None: + return Lo("?lo40_%02X" % f, w) + l = Lo(n, w) + if n == "iaddi": + imm = (w >> 6) & 31 + l.imm = imm - 32 if imm & 16 else imm + return l + op2 = ((w >> 6) & 31) << 2 | (w & 3) + n = _LOWER_S2.get(op2) + if n is None: + return Lo("?lo2_%02X" % op2, w) + return Lo(n, w) + n = _LOWER.get(op) + if n is None: + return Lo("?lo%02X" % op, w) + l = Lo(n, w) + if n in ("lq", "sq", "ilw", "isw", "b", "bal", "ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez"): + l.imm = s11(w) + elif n in ("iaddiu", "isubiu"): + l.imm = ((w >> 10) & 0x7800) | (w & 0x7FF) + elif n in ("fceq", "fcset", "fcand", "fcor"): + l.imm = w & 0xFFFFFF + elif n in ("fseq", "fsset", "fsand", "fsor"): + l.imm = ((w >> 10) & 0x800) | (w & 0x7FF) + return l + + +def decode_pair(lo, hi): + """returns (upper, lower or None, flags dict, i_imm or None)""" + up = decode_upper(hi) + fl = {"I": (hi >> 31) & 1, "E": (hi >> 30) & 1, "M": (hi >> 29) & 1, "D": (hi >> 28) & 1, "T": (hi >> 27) & 1} + if fl["I"]: + return up, None, fl, lo + return up, decode_lower(lo), fl, None diff --git a/games/rac1/tools/vu_recomp.py b/games/rac1/tools/vu_recomp.py new file mode 100644 index 0000000..5524abb --- /dev/null +++ b/games/rac1/tools/vu_recomp.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python3 +"""vu_recomp.py GEN_DIR DECOMP_REPO [--dis OUT_DIR] + +Ahead-of-time translator for the game's VU microprograms (VU0 and VU1 micro +mode). The microcode lives in the executable's .vutext section, listed by its +.DVP.ovlytab overlay table: one entry per MPG upload (VU address, size, load +address). Uploads of one program image share a group id in the overlay's +name (".DVP.overlay....."). + +Each image is translated to one C function over an RcVu0 (host/include/vu.h): +a switch on the start address, then one labelled piece of C per instruction +pair. The function is compiled with the rest of the host program; nothing is +decoded at run time. Images that fit VU0's 4 KB micro memory are translated +for both VUs, the rest for VU1 only. The bytes of every upload are kept too, +so the runtime (host/src/gfx/vu.c) can tell which image is in micro memory +after the game's MPG uploads. + +Semantics kept: + - upper and lower ops of a pair both read the registers as they were before + the pair; the upper op's result wins when both write the same register; + with the I bit the lower word is LOI's immediate (the pair's own upper + op still reads the old I: the game's sine code depends on it). + - branches and E (end) have one delay slot pair. + - FMAC results that overflow are +-MAX, as on the VU (which has no inf + or NaN): x86 would give inf, and then NaN (inf * 0, inf - inf), where + the VU keeps finite numbers (vu_fc). + - MAC/status/clip flags reach FSAND/FMAND/FCAND... 4 cycles after the op + issues, Q 7 cycles after DIV/SQRT and 13 after RSQRT: the code counts + cycles, including the stalls on VF registers that are still being written + (4-cycle latency, per component), which are worked out here. + +Writes GEN_DIR/vu_prog_.c (one per image and VU) and GEN_DIR/vu_table.c. +""" +import os, re, struct, sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from vu_isa import decode_pair, BRANCHES, XYZW + +GEN, REPO = sys.argv[1], sys.argv[2] +DIS = sys.argv[sys.argv.index("--dis") + 1] if "--dis" in sys.argv else None +EXE = os.environ.get("UYA_EXE") or os.path.join(REPO, "baserom", "SCES_509.16") # build.py: the copy read from the disc image +img = open(EXE, "rb").read() + +# ---- the executable's sections ------------------------------------------------ +e_shoff, = struct.unpack_from(" ti: + qs = qready - ti + ti += qs + if l is not None and l.name in ("div", "sqrt", "rsqrt"): + qready = ti + (13 if l.name == "rsqrt" else 7) + stall[pc] = s + qs + for x in w1 + w2: + ready[x] = ti + 4 + t = ti + # a new straight-line run starts after an unconditional jump's or E's delay slot + prev_end = False + p = pc - 8 + if p in dec: + pu, pl, pf, pimm = dec[p] + if pf["E"] or (pl is not None and pl.name in ("b", "jr")): + prev_end = True + + used_q = False + + # ---- branch VI delay: a conditional branch (or JR/JALR) whose VI register + # was written by the instruction right before it reads the value the + # register had before that write (before the earliest of a run of such + # writes, up to 4 back), unless the branch stalls or the write reads flags + # (FCAND and friends). This is the hardware's integer pipeline; PCSX2's + # microVU models it the same way (analyzeBranchVI). ---- + FLAGREAD = {"fcand", "fcor", "fceq", "fcget", "fsand", "fsor", "fseq", "fmand", "fmor", "fmeq"} + def vi_written(lw): + if lw is None: return 0 + n = lw.name + if n in ("iadd", "isub", "iand", "ior"): return lw.id_ & 15 + if n in ("iaddi", "iaddiu", "isubiu", "ilw", "ilwr", "mtir", "xtop", "xitop", "bal", "jalr"): return lw.it & 15 + if n in ("lqi", "lqd"): return lw.is_ & 15 + if n in ("sqi", "sqd"): return lw.it & 15 + return 0 + targets = set() + for pc_ in pcs: + l_ = dec[pc_][1] + if l_ is not None and l_.name in ("b", "bal", "ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez"): + targets.add((pc_ + 8 + l_.imm * 8) & PCMASK) + vib_save, vib_use = {}, {} + for pc_ in pcs: + l_ = dec[pc_][1] + if l_ is None or l_.name not in ("ibeq", "ibne", "ibltz", "ibgtz", "iblez", "ibgez", "jr", "jalr"): continue + if stall[pc_] or pc_ in targets: continue + regs = [l_.is_ & 15] + ([l_.it & 15] if l_.name in ("ibeq", "ibne") else []) + for x in regs: + if not x: continue + p_ = pc_ - 8 + if p_ not in dec or dec[p_][1] is None or dec[p_][1].name in FLAGREAD or vi_written(dec[p_][1]) != x: continue + j_ = p_ + for k_ in range(3): + q_ = j_ - 8 + if j_ in targets or q_ not in dec or stall[j_]: break + lq_ = dec[q_][1] + if lq_ is None or lq_.name in FLAGREAD or vi_written(lq_) != x: break + j_ = q_ + vib_save[j_] = x + vib_use[pc_] = x + break + + def VI(r): + r &= 15 + return "0u" if r == 0 else "v->vi[%d]" % r + + def setvi(r, e): + r &= 15 + return "" if r == 0 else "v->vi[%d] = (%s) & 0xFFFF;" % (r, e) + + def U32(r): + return "((u32 *)v->vf[%d])" % r + + def upper(pc, u): + """(compute, commit) code for the upper op""" + nonlocal used_q + k = u.kind + if k == "nop": + return "", "" + if k == "bad": + im.warn.append("%04X upper %s" % (pc, u.name)) + return 'vu_bad(%d, 0x%04X, "%s");' % (vu, pc, u.name), "" + comp, commit = [], [] + F = lambda r, c: "v->vf[%d][%d]" % (r, c) + if k == "arith": + base = u.base if u.acc else u.name + if u.bc == "q": + used_q = True + comp.append("f32 qv = vu_q(v, cyc);") + + def B(c): + if u.bc in ("x", "y", "z", "w"): return F(u.ft, XYZW.index(u.bc)) + if u.bc == "q": return "qv" + if u.bc == "i": return "v->i" + return F(u.ft, c) + for c in u.comps: + a, b = F(u.fs, c), B(c) + e = {"add": "%s + %s" % (a, b), "sub": "%s - %s" % (a, b), "mul": "%s * %s" % (a, b), + "madd": "v->acc[%d] + vu_fc(%s * %s)" % (c, a, b), "msub": "v->acc[%d] - vu_fc(%s * %s)" % (c, a, b), + "max": "fmaxf(%s, %s)" % (a, b), "mini": "fminf(%s, %s)" % (a, b)}[base] + comp.append("f32 u%d = %s;" % (c, e)) + for c in u.comps: + if u.acc: commit.append("v->acc[%d] = vu_fc(u%d);" % (c, c)) + elif u.fd: commit.append("v->vf[%d][%d] = vu_fc(u%d);" % (u.fd, c, c)) + if reads_sm and base in ("add", "sub", "mul", "madd", "msub"): + commit.append("vu_fl_push(v, cyc, %s);" % (" | ".join("vu_macbits(u%d, %d)" % (c, c) for c in u.comps) or "0")) + elif k in ("opmula", "opmsub"): + ex = {0: (1, 2), 1: (2, 0), 2: (0, 1)} + cs = [c for c in u.comps if c < 3] + for c in cs: + a, b = ex[c] + p = "%s * %s" % (F(u.fs, a), F(u.ft, b)) + comp.append("f32 u%d = %s;" % (c, p if k == "opmula" else "v->acc[%d] - vu_fc(%s)" % (c, p))) + for c in cs: + if k == "opmula": commit.append("v->acc[%d] = vu_fc(u%d);" % (c, c)) + elif u.fd: commit.append("v->vf[%d][%d] = vu_fc(u%d);" % (u.fd, c, c)) + if reads_sm: + commit.append("vu_fl_push(v, cyc, %s);" % (" | ".join("vu_macbits(u%d, %d)" % (c, c) for c in cs) or "0")) + elif k == "itof": + for c in u.comps: + comp.append("u32 n%d = vu_itof(%s[%d], %.9ef);" % (c, U32(u.fs), c, 1.0 / (1 << u.shift))) + if u.ft: + commit += ["%s[%d] = n%d;" % (U32(u.ft), c, c) for c in u.comps] + elif k == "ftoi": + for c in u.comps: + comp.append("s32 n%d = vu_ftoi(%s, %.1ff);" % (c, F(u.fs, c), float(1 << u.shift))) + if u.ft: + commit += ["%s[%d] = (u32)n%d;" % (U32(u.ft), c, c) for c in u.comps] + elif k == "abs": + for c in u.comps: + comp.append("u32 n%d = %s[%d] & 0x7FFFFFFFu;" % (c, U32(u.fs), c)) + if u.ft: + commit += ["%s[%d] = n%d;" % (U32(u.ft), c, c) for c in u.comps] + elif k == "clip": + comp.append("f32 cw = v->vf[%d][3];" % u.ft) + comp.append("f32 cs[3] = { v->vf[%d][0], v->vf[%d][1], v->vf[%d][2] };" % (u.fs, u.fs, u.fs)) + commit.append("vu_clipw(v, cyc, cs, cw);") + return " ".join(comp), " ".join(commit) + + def lower(pc, l): + """(pre, code, kind, target) ; kind in None, 'b' (static target), 'j' (npc)""" + nonlocal used_q + n = l.name + c0 = l.comps[0] if l.comps else 0 + if n.startswith("?"): + im.warn.append("%04X lower %s" % (pc, n)) + return "", 'vu_bad(%d, 0x%04X, "%s");' % (vu, pc, n), None, None + if n == "move": + if not l.comps or not l.ft: return "", "", None, None + return "", " ".join("%s[%d] = %s[%d];" % (U32(l.ft), c, U32(l.fs), c) for c in l.comps), None, None + if n == "mr32": + if not l.ft: return "", "", None, None + s = "{ u32 m0 = %s[1], m1 = %s[2], m2 = %s[3], m3 = %s[0]; " % (U32(l.fs), U32(l.fs), U32(l.fs), U32(l.fs)) + return "", s + " ".join("%s[%d] = m%d;" % (U32(l.ft), c, c) for c in l.comps) + " }", None, None + if n in ("lq", "lqi", "lqd"): + reg = l.is_ + pre = setvi(reg, VI(reg) + " - 1") + " " if n == "lqd" else "" + post = " " + setvi(reg, VI(reg) + " + 1") if n == "lqi" else "" + addr = VI(reg) + (" + %d" % l.imm if n == "lq" else "") + body = " ".join("%s[%d] = m[%d];" % (U32(l.ft), c, c) for c in l.comps) if l.ft else "" + return "", "%s{ const u32 *m = %s(%s); %s }%s" % (pre, MEM, addr, body, post), None, None + if n in ("sq", "sqi", "sqd"): + reg = l.it + pre = setvi(reg, VI(reg) + " - 1") + " " if n == "sqd" else "" + post = " " + setvi(reg, VI(reg) + " + 1") if n == "sqi" else "" + addr = VI(reg) + (" + %d" % l.imm if n == "sq" else "") + body = " ".join("m[%d] = %s[%d];" % (c, U32(l.fs), c) for c in l.comps) + return "", "%s{ u32 *m = %s(%s); %s }%s" % (pre, MEM, addr, body, post), None, None + if n in ("ilw", "ilwr"): + addr = VI(l.is_) + (" + %d" % l.imm if n == "ilw" else "") + return "", setvi(l.it, "%s(%s)[%d]" % (MEM, addr, c0)), None, None + if n in ("isw", "iswr"): + addr = VI(l.is_) + (" + %d" % l.imm if n == "isw" else "") + return "", "{ u32 *m = %s(%s); %s }" % (MEM, addr, " ".join("m[%d] = %s;" % (c, VI(l.it)) for c in l.comps)), None, None + if n in ("iadd", "isub", "iand", "ior"): + op = {"iadd": "+", "isub": "-", "iand": "&", "ior": "|"}[n] + return "", setvi(l.id_, "%s %s %s" % (VI(l.is_), op, VI(l.it))), None, None + if n == "iaddi": + return "", setvi(l.it, "%s + (%d)" % (VI(l.is_), l.imm)), None, None + if n == "iaddiu": + return "", setvi(l.it, "%s + %d" % (VI(l.is_), l.imm)), None, None + if n == "isubiu": + return "", setvi(l.it, "%s - %d" % (VI(l.is_), l.imm)), None, None + if n == "mtir": + return "", setvi(l.it, "%s[%d]" % (U32(l.fs), l.fsf)), None, None + if n == "mfir": + if not l.ft: return "", "", None, None + return "", " ".join("%s[%d] = (u32)(s32)(s16)%s;" % (U32(l.ft), c, VI(l.is_)) for c in l.comps), None, None + F = lambda r, c: "v->vf[%d][%d]" % (r, c) + if n == "div": + used_q = True + return "cyc = vu_qwait(v, cyc);", "vu_qset(v, cyc, 7, vu_div(%s, %s));" % (F(l.fs, l.fsf), F(l.ft, l.ftf)), None, None + if n == "sqrt": + used_q = True + return "cyc = vu_qwait(v, cyc);", "vu_qset(v, cyc, 7, vu_sqrt(%s));" % F(l.ft, l.ftf), None, None + if n == "rsqrt": + used_q = True + return "cyc = vu_qwait(v, cyc);", "vu_qset(v, cyc, 13, vu_rsqrt(%s, %s));" % (F(l.fs, l.fsf), F(l.ft, l.ftf)), None, None + if n == "waitq": + return "cyc = vu_qwait(v, cyc);", "", None, None + if n == "fcand": return "", setvi(1, "(vu_clip(v, cyc) & 0x%X) != 0" % l.imm), None, None + if n == "fcor": return "", setvi(1, "((vu_clip(v, cyc) | 0x%X) & 0xFFFFFF) == 0xFFFFFF" % l.imm), None, None + if n == "fceq": return "", setvi(1, "(vu_clip(v, cyc) & 0xFFFFFF) == 0x%X" % l.imm), None, None + if n == "fcset": return "", "vu_fcset(v, cyc, 0x%X);" % l.imm, None, None + if n == "fcget": return "", setvi(l.it, "vu_clip(v, cyc) & 0xFFF"), None, None + if n == "fsand": return "", setvi(l.it, "vu_status(v, cyc) & 0x%X" % l.imm), None, None + if n == "fsor": return "", setvi(l.it, "vu_status(v, cyc) | 0x%X" % l.imm), None, None + if n == "fseq": return "", setvi(l.it, "(vu_status(v, cyc) & 0xFFF) == 0x%X" % l.imm), None, None + if n == "fsset": return "", "vu_fsset(v, cyc, 0x%X);" % l.imm, None, None + if n == "fmand": return "", setvi(l.it, "%s & vu_mac(v, cyc)" % VI(l.is_)), None, None + if n == "fmor": return "", setvi(l.it, "%s | vu_mac(v, cyc)" % VI(l.is_)), None, None + if n == "fmeq": return "", setvi(l.it, "(%s & 0xFFFF) == vu_mac(v, cyc)" % VI(l.is_)), None, None + if n == "xgkick": + if vu != 1: + im.warn.append("%04X xgkick on VU0" % pc) + return "", 'vu_bad(0, 0x%04X, "xgkick");' % pc, None, None + return "", "vu_xgkick(%s);" % VI(l.is_), None, None + if n == "xtop": return "", setvi(l.it, "vu_vif_top(%d)" % vu), None, None + if n == "xitop": return "", setvi(l.it, "vu_vif_itop(%d)" % vu), None, None + if n in BRANCHES: + link = (pc + 16) // 8 + if n == "b": + return "", "", "b", (pc + 8 + l.imm * 8) & PCMASK + if n == "bal": + return "", setvi(l.it, "%d" % link), "b", (pc + 8 + l.imm * 8) & PCMASK + JV = "vib" if vib_use.get(pc) == (l.is_ & 15) else VI(l.is_) + if n == "jr": + return "", "npc = (%s << 3) & 0x%X;" % (JV, PCMASK), "j", None + if n == "jalr": + return "", "npc = (%s << 3) & 0x%X; %s" % (JV, PCMASK, setvi(l.it, "%d" % link)), "j", None + BV = (lambda r: "vib" if vib_use.get(pc) == (r & 15) else VI(r)) + cond = {"ibeq": "%s == %s" % (BV(l.it), BV(l.is_)), "ibne": "%s != %s" % (BV(l.it), BV(l.is_)), + "ibltz": "(s16)%s < 0" % BV(l.is_), "ibgtz": "(s16)%s > 0" % BV(l.is_), + "iblez": "(s16)%s <= 0" % BV(l.is_), "ibgez": "(s16)%s >= 0" % BV(l.is_)}[n] + return "", "bt = %s;" % cond, "c", (pc + 8 + l.imm * 8) & PCMASK + im.warn.append("%04X lower %s not translated" % (pc, n)) + return "", 'vu_bad(%d, 0x%04X, "%s");' % (vu, pc, n), None, None + + def pair(pc): + """C for one pair; returns (code, flow kind, target)""" + u, l, fl, imm = dec[pc] + out = ["VU_TRACE(0x%04X); cyc += %d;" % (pc, 1 + stall[pc])] + if pc in vib_save: out.append("vib = %s;" % VI(vib_save[pc])) + uc, ucommit = upper(pc, u) + pre = code = "" + kind = tgt = None + if l is not None: + pre, code, kind, tgt = lower(pc, l) + if pre: out.append(pre) + if imm is not None: # LOI: the pair's upper op still reads the old I + code = "v->i = vu_f(0x%08Xu);" % imm + if uc or ucommit: + out.append("{ %s %s %s }" % (uc, code, ucommit)) + elif code: + out.append(code) + return " ".join(x for x in out if x), kind, tgt, fl["E"] + + body = [] + lab = lambda pc: "L_%04X" % pc + import re as _re + def idle(pc): # a pair that does nothing but count its cycle + c_, k_, t_, e_ = pair(pc) + return k_ is None and not e_ and not _re.sub(r"VU_TRACE\(0x[0-9A-F]+\);|cyc \+= \d+;|\s", "", c_) + for pc in pcs: + code, kind, tgt, ebit = pair(pc) + body.append("%s: %s" % (lab(pc), code)) + nxt = pc + 8 + if kind is None and not ebit: + if nxt not in dec: + body.append(" vu_bad(%d, 0x%04X, \"ran off the end\"); goto done;" % (vu, nxt)) + continue + # a branch or the end: its delay slot pair runs here as well + if nxt in dec: + dcode, dkind, dtgt, debit = pair(nxt) + if dkind is not None: + im.warn.append("%04X branch in a delay slot (ignored)" % nxt) + if debit and not ebit: + im.warn.append("%04X E bit in a branch delay slot" % nxt) + body.append(" /* delay slot */ " + dcode) + else: + im.warn.append("%04X delay slot outside the image" % pc) + if ebit: + if kind is not None: + im.warn.append("%04X branch with the E bit" % pc) + body.append(" v->tpc = 0x%04X; goto done;" % ((pc + 16) & PCMASK)) + continue + if kind == "b" and tgt == pc - 16 and tgt in dec and pair(tgt)[1] == "c" and idle(tgt + 8) and (nxt not in dec or idle(nxt)) \ + and "vi[" in pair(tgt)[0] and not _re.search(r"vf\[|acc\[|VU\d_MEM", pair(tgt)[0]): + # a wait loop on a VI register the EE writes (ctc2): park here and + # let the EE resume the program (rc_vu0_resume) when it writes one + body.append(" v->tpc = 0x%04X; v->susp = 1; goto done; /* wait for the EE */" % tgt) + elif kind == "b": + body.append(" goto %s;" % lab(tgt) if tgt in dec else " pc = 0x%04X; goto dispatch;" % tgt) + elif kind == "c": + body.append(" if (bt) %s" % ("goto %s;" % lab(tgt) if tgt in dec else "{ pc = 0x%04X; goto dispatch; }" % tgt)) + if nxt + 8 in dec: + body.append(" goto %s;" % lab(nxt + 8)) + else: + body.append(" goto done;") + elif kind == "j": + body.append(" pc = npc; goto dispatch;") + + name = "vup_%s_vu%d" % (im.gid, vu) + out = ["/* VU%d microprogram image %s, translated from the executable by tools/vu_recomp.py */" % (vu, im.gid), + '#include "vu.h"', "", + "void %s(RcVu0 *v, u32 pc)" % name, "{", + " u32 cyc = 0, npc = 0, vib = 0; int bt = 0; (void)npc; (void)bt; (void)vib;", + " v->st_int = v->status; v->cl_int = v->clip;", + "dispatch:", + " switch (pc & 0x%X) {" % PCMASK] + for pc in pcs: + out.append(" case 0x%04X: goto %s;" % (pc, lab(pc))) + out.append(' default: vu_bad(%d, pc, "start address"); goto done;' % vu) + out.append(" }") + out += body + out += ["done:", " vu_settle(v);", "}"] + return name, "\n".join(out) + "\n" + + +def extra_images(): + """Microcode the game uploads that is not in .vutext (none is known for + RAC1): the runtime reports such an upload and, with UYA_VUDUMP=dir, saves + it as vu__.bin. Files of that name in RAC1_VU_EXTRA (or + the vu_extra folder next to build.py) are translated as images of their + own, one upload each.""" + d = os.environ.get("RAC1_VU_EXTRA") or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "vu_extra") + out = [] + if not os.path.isdir(d): + return out + for fn in sorted(os.listdir(d)): + m = re.fullmatch(r"vu([01])_([0-9A-Fa-f]{4})_([0-9A-Fa-f]{8})\.bin", fn) + if not m: + continue + data = open(os.path.join(d, fn), "rb").read() + data = data[:len(data) // 8 * 8] + out.append(Image("x%s_%s" % (m.group(2).upper(), m.group(3).upper()), [(int(m.group(2), 16), data)], int(m.group(1)))) + return out + + +def main(): + os.makedirs(GEN, exist_ok=True) + images = [] + for g in order: + blocks = groups[g] + size = max(vma + len(d) for vma, d in blocks) + kicks = any(decode_pair(*struct.unpack_from(" \ No newline at end of file diff --git a/launcher/assets/svg/app_icon_small_16-32.svg b/launcher/assets/svg/app_icon_small_16-32.svg new file mode 100644 index 0000000..7390856 --- /dev/null +++ b/launcher/assets/svg/app_icon_small_16-32.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_circle.svg b/launcher/assets/svg/controller_colour/ps_circle.svg new file mode 100644 index 0000000..1c8d976 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_cross.svg b/launcher/assets/svg/controller_colour/ps_cross.svg new file mode 100644 index 0000000..ef5a398 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_l1.svg b/launcher/assets/svg/controller_colour/ps_l1.svg new file mode 100644 index 0000000..27e09bf --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_l1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_l2.svg b/launcher/assets/svg/controller_colour/ps_l2.svg new file mode 100644 index 0000000..d129a45 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_l2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_r1.svg b/launcher/assets/svg/controller_colour/ps_r1.svg new file mode 100644 index 0000000..202d5fd --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_r1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_r2.svg b/launcher/assets/svg/controller_colour/ps_r2.svg new file mode 100644 index 0000000..d7405d6 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_r2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_select.svg b/launcher/assets/svg/controller_colour/ps_select.svg new file mode 100644 index 0000000..4b19722 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_square.svg b/launcher/assets/svg/controller_colour/ps_square.svg new file mode 100644 index 0000000..98a4c63 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_start.svg b/launcher/assets/svg/controller_colour/ps_start.svg new file mode 100644 index 0000000..8f01fa9 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/ps_triangle.svg b/launcher/assets/svg/controller_colour/ps_triangle.svg new file mode 100644 index 0000000..a0c0b20 --- /dev/null +++ b/launcher/assets/svg/controller_colour/ps_triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_a.svg b/launcher/assets/svg/controller_colour/xbox_a.svg new file mode 100644 index 0000000..398166a --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_b.svg b/launcher/assets/svg/controller_colour/xbox_b.svg new file mode 100644 index 0000000..972fb9e --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_b.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_lb.svg b/launcher/assets/svg/controller_colour/xbox_lb.svg new file mode 100644 index 0000000..d396338 --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_lb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_lt.svg b/launcher/assets/svg/controller_colour/xbox_lt.svg new file mode 100644 index 0000000..173a6e2 --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_lt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_menu.svg b/launcher/assets/svg/controller_colour/xbox_menu.svg new file mode 100644 index 0000000..0d5d7e9 --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_rb.svg b/launcher/assets/svg/controller_colour/xbox_rb.svg new file mode 100644 index 0000000..30ba5dc --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_rb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_rt.svg b/launcher/assets/svg/controller_colour/xbox_rt.svg new file mode 100644 index 0000000..d2f44ad --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_rt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_view.svg b/launcher/assets/svg/controller_colour/xbox_view.svg new file mode 100644 index 0000000..cd36832 --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_x.svg b/launcher/assets/svg/controller_colour/xbox_x.svg new file mode 100644 index 0000000..a815e4a --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_colour/xbox_y.svg b/launcher/assets/svg/controller_colour/xbox_y.svg new file mode 100644 index 0000000..4278d25 --- /dev/null +++ b/launcher/assets/svg/controller_colour/xbox_y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_circle.svg b/launcher/assets/svg/controller_white/ps_circle.svg new file mode 100644 index 0000000..6e459c9 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_cross.svg b/launcher/assets/svg/controller_white/ps_cross.svg new file mode 100644 index 0000000..aeabaa3 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_l1.svg b/launcher/assets/svg/controller_white/ps_l1.svg new file mode 100644 index 0000000..4baac26 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_l1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_l2.svg b/launcher/assets/svg/controller_white/ps_l2.svg new file mode 100644 index 0000000..9ee9b1e --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_l2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_r1.svg b/launcher/assets/svg/controller_white/ps_r1.svg new file mode 100644 index 0000000..bb06e60 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_r1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_r2.svg b/launcher/assets/svg/controller_white/ps_r2.svg new file mode 100644 index 0000000..83807c7 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_r2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_select.svg b/launcher/assets/svg/controller_white/ps_select.svg new file mode 100644 index 0000000..0a94d3b --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_square.svg b/launcher/assets/svg/controller_white/ps_square.svg new file mode 100644 index 0000000..7ee693f --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_start.svg b/launcher/assets/svg/controller_white/ps_start.svg new file mode 100644 index 0000000..c96afab --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/ps_triangle.svg b/launcher/assets/svg/controller_white/ps_triangle.svg new file mode 100644 index 0000000..cca2e40 --- /dev/null +++ b/launcher/assets/svg/controller_white/ps_triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_a.svg b/launcher/assets/svg/controller_white/xbox_a.svg new file mode 100644 index 0000000..884009b --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_b.svg b/launcher/assets/svg/controller_white/xbox_b.svg new file mode 100644 index 0000000..74d57c6 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_b.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_lb.svg b/launcher/assets/svg/controller_white/xbox_lb.svg new file mode 100644 index 0000000..5a79ee1 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_lb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_lt.svg b/launcher/assets/svg/controller_white/xbox_lt.svg new file mode 100644 index 0000000..9cb74e1 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_lt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_menu.svg b/launcher/assets/svg/controller_white/xbox_menu.svg new file mode 100644 index 0000000..5d9dbcb --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_rb.svg b/launcher/assets/svg/controller_white/xbox_rb.svg new file mode 100644 index 0000000..6daf043 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_rb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_rt.svg b/launcher/assets/svg/controller_white/xbox_rt.svg new file mode 100644 index 0000000..7ce4a2f --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_rt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_view.svg b/launcher/assets/svg/controller_white/xbox_view.svg new file mode 100644 index 0000000..038c7ac --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_x.svg b/launcher/assets/svg/controller_white/xbox_x.svg new file mode 100644 index 0000000..5a3a017 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/controller_white/xbox_y.svg b/launcher/assets/svg/controller_white/xbox_y.svg new file mode 100644 index 0000000..dd109b1 --- /dev/null +++ b/launcher/assets/svg/controller_white/xbox_y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/default_banner.svg b/launcher/assets/svg/default_banner.svg new file mode 100644 index 0000000..05074cf --- /dev/null +++ b/launcher/assets/svg/default_banner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/empty_library.svg b/launcher/assets/svg/empty_library.svg new file mode 100644 index 0000000..bacae9b --- /dev/null +++ b/launcher/assets/svg/empty_library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/add.svg b/launcher/assets/svg/icons/add.svg new file mode 100644 index 0000000..f04ab0b --- /dev/null +++ b/launcher/assets/svg/icons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/build.svg b/launcher/assets/svg/icons/build.svg new file mode 100644 index 0000000..f20533d --- /dev/null +++ b/launcher/assets/svg/icons/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/cancel.svg b/launcher/assets/svg/icons/cancel.svg new file mode 100644 index 0000000..f3eabe6 --- /dev/null +++ b/launcher/assets/svg/icons/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/game_folder.svg b/launcher/assets/svg/icons/game_folder.svg new file mode 100644 index 0000000..f870bd6 --- /dev/null +++ b/launcher/assets/svg/icons/game_folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/open_log.svg b/launcher/assets/svg/icons/open_log.svg new file mode 100644 index 0000000..9d21df3 --- /dev/null +++ b/launcher/assets/svg/icons/open_log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/play.svg b/launcher/assets/svg/icons/play.svg new file mode 100644 index 0000000..b58c538 --- /dev/null +++ b/launcher/assets/svg/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/rebuild.svg b/launcher/assets/svg/icons/rebuild.svg new file mode 100644 index 0000000..2fb4a37 --- /dev/null +++ b/launcher/assets/svg/icons/rebuild.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/remove.svg b/launcher/assets/svg/icons/remove.svg new file mode 100644 index 0000000..7a360e1 --- /dev/null +++ b/launcher/assets/svg/icons/remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/scan.svg b/launcher/assets/svg/icons/scan.svg new file mode 100644 index 0000000..f494afa --- /dev/null +++ b/launcher/assets/svg/icons/scan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/icons/settings.svg b/launcher/assets/svg/icons/settings.svg new file mode 100644 index 0000000..533f5fb --- /dev/null +++ b/launcher/assets/svg/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/window_background_tile.svg b/launcher/assets/svg/window_background_tile.svg new file mode 100644 index 0000000..b450a34 --- /dev/null +++ b/launcher/assets/svg/window_background_tile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/wordmark.svg b/launcher/assets/svg/wordmark.svg new file mode 100644 index 0000000..847b432 --- /dev/null +++ b/launcher/assets/svg/wordmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/assets/svg/wordmark_text_only.svg b/launcher/assets/svg/wordmark_text_only.svg new file mode 100644 index 0000000..8c282bc --- /dev/null +++ b/launcher/assets/svg/wordmark_text_only.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/build_launcher.py b/launcher/build_launcher.py new file mode 100644 index 0000000..a7c2dd1 --- /dev/null +++ b/launcher/build_launcher.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""build_launcher.py: builds Project Solana.exe (the launcher) with zig. + +usage: python launcher/build_launcher.py [OUT_DIR] (default: dist/) +Needs Python 3 and zig (pip install ziglang, or zig on PATH).""" +import os, shutil, subprocess, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +out = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.path.join(HERE, "..", "dist") +os.makedirs(out, exist_ok=True) +zig = [shutil.which("zig")] if shutil.which("zig") else [sys.executable, "-m", "ziglang"] +exe = os.path.join(out, "Project Solana.exe") +cmd = zig + ["cc", "-target", "x86-windows-gnu", "-O2", "-s", "-municode", "-Wl,--subsystem,windows", "-o", exe, + "solana.c", "solana.rc", "-lgdi32", "-luser32", "-lshell32", "-lcomdlg32", "-lole32", "-lmsimg32", "-lwinhttp", "-lbcrypt", "-lwindowscodecs", "-luuid"] +r = subprocess.run(cmd, cwd=HERE) +if r.returncode: sys.exit("build failed") +print("built", exe) diff --git a/launcher/solana.c b/launcher/solana.c new file mode 100644 index 0000000..61b1ff4 --- /dev/null +++ b/launcher/solana.c @@ -0,0 +1,943 @@ +/* Project Solana launcher: a game library for the static recompilations. + * + * Add disc images (Add disc, Scan folder, or drag and drop). Each one is identified by + * the boot executable its SYSTEM.CNF names (BOOT2) and that executable's MD5, then + * matched against the games this build knows. A supported disc gets a Play button that + * starts that game's wrapper (rac1_wrapper.exe - ) in its own folder with the + * settings below as environment variables. The library and settings are kept in + * solana.ini next to this program. + * + * Plain Win32 + GDI, everything custom-drawn (double-buffered), DPI-aware. + * Build: tools/build_frontend.py (zig cc, x86-windows-gnu). */ +#ifndef UNICODE +#define UNICODE +#endif +#ifndef _UNICODE +#define _UNICODE +#endif +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- the games this build knows ---------------------------------------------------- */ +typedef struct { + const char *serial; /* BOOT2's file name, as in the disc's root */ + const wchar_t *title, *region; + const char *md5; /* the executable this recompilation was made from (0: any) */ + const wchar_t *dir; /* the game's folder, relative to this program's */ + const wchar_t *exe; /* its wrapper in that folder (0: not supported yet) */ + const wchar_t *kit; /* its GitHub release tag and folder under games\ (the Build button) */ + const wchar_t *const *levels; +} game_t; + +static const wchar_t *const rac1_levels[] = { + L"Veldin", L"Novalis", L"Aridia", L"Kerwan", L"Eudora", L"Rilgar", L"Blarg Station", L"Umbris", + L"Batalia", L"Gaspar", L"Orxon", L"Pokitaru", L"Hoven", L"Gemlik Base", L"Oltanis", L"Quartu", + L"Kalebo III", L"Drek's Fleet", L"Veldin (Drek)", 0 }; + +static const game_t games[] = { + { "SCES_509.16", L"Ratchet & Clank", L"PAL", "d86429e2eca4e1da7d7f1cc656af2574", L"games\\rac1\\build\\windows", L"rac1_wrapper.exe", L"rac1", rac1_levels }, + { "SCUS_971.99", L"Ratchet & Clank", L"NTSC-U", 0, 0, 0, 0, 0 }, + { "SCPS_150.37", L"Ratchet & Clank", L"NTSC-J", 0, 0, 0, 0, 0 }, + { "SCES_516.07", L"Ratchet & Clank 2", L"PAL", 0, 0, 0, 0, 0 }, + { "SCUS_972.68", L"Ratchet & Clank: Going Commando", L"NTSC-U", 0, 0, 0, 0, 0 }, + { "SCES_524.56", L"Ratchet & Clank 3", L"PAL", 0, 0, 0, 0, 0 }, + { "SCUS_973.53", L"Ratchet & Clank: Up Your Arsenal", L"NTSC-U", 0, 0, 0, 0, 0 }, + { "SCES_532.85", L"Ratchet: Gladiator", L"PAL", 0, 0, 0, 0, 0 }, + { "SCUS_974.65", L"Ratchet: Deadlocked", L"NTSC-U", 0, 0, 0, 0, 0 }, +}; +#define NGAMES (int)(sizeof games / sizeof games[0]) + +/* ---- the library ------------------------------------------------------------------- */ +enum { ST_OK, ST_REVISION, ST_NOTYET, ST_UNKNOWN, ST_NOTDISC, ST_MISSING }; +typedef struct { + wchar_t path[MAX_PATH]; + char serial[32], md5[33]; + int game; /* index into games[], or -1 */ + int status; +} disc_t; +#define MAXDISCS 64 +static disc_t discs[MAXDISCS]; +static int ndiscs, sel = -1; + +/* settings */ +static int set_upscale = 1, set_vsync, set_nomtgs, set_noglt, set_fpslog, set_capsec = 3, set_level = -1, set_minimize = 1; + +static wchar_t base_dir[MAX_PATH], ini_path[MAX_PATH]; +static HWND hwnd; +static float dpi = 1.0f; +#define S(x) ((int)((x) * dpi + 0.5f)) + +/* ---- MD5 (RFC 1321) ----------------------------------------------------------------- */ +typedef struct { unsigned a, b, c, d; unsigned long long n; unsigned char buf[64]; } md5_t; +static const unsigned md5_k[64] = { + 0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821, + 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a, + 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665, + 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391 }; +static const unsigned char md5_r[64] = { 7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20, + 4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21 }; +static void md5_block(md5_t *m, const unsigned char *p) +{ + unsigned w[16], a = m->a, b = m->b, c = m->c, d = m->d; + for (int i = 0; i < 16; i++) w[i] = p[i * 4] | p[i * 4 + 1] << 8 | p[i * 4 + 2] << 16 | (unsigned)p[i * 4 + 3] << 24; + for (int i = 0; i < 64; i++) { + unsigned f, g; + if (i < 16) { f = (b & c) | (~b & d); g = i; } + else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) & 15; } + else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) & 15; } + else { f = c ^ (b | ~d); g = (7 * i) & 15; } + unsigned t = d; d = c; c = b; + unsigned x = a + f + md5_k[i] + w[g]; + b = b + ((x << md5_r[i]) | (x >> (32 - md5_r[i]))); + a = t; + } + m->a += a; m->b += b; m->c += c; m->d += d; +} +static void md5_init(md5_t *m) { m->a = 0x67452301; m->b = 0xefcdab89; m->c = 0x98badcfe; m->d = 0x10325476; m->n = 0; } +static void md5_add(md5_t *m, const unsigned char *p, size_t n) +{ + while (n--) { m->buf[m->n++ & 63] = *p++; if (!(m->n & 63)) md5_block(m, m->buf); } +} +static void md5_hex(md5_t *m, char out[33]) +{ + unsigned long long bits = m->n * 8; + unsigned char one = 0x80, zero = 0, len[8]; + md5_add(m, &one, 1); + while ((m->n & 63) != 56) md5_add(m, &zero, 1); + for (int i = 0; i < 8; i++) len[i] = (unsigned char)(bits >> (8 * i)); + md5_add(m, len, 8); + unsigned v[4] = { m->a, m->b, m->c, m->d }; + for (int i = 0; i < 16; i++) sprintf(out + 2 * i, "%02x", (v[i / 4] >> (8 * (i & 3))) & 255); + out[32] = 0; +} + +/* ---- disc images (ISO 9660, 2048-byte sectors) -------------------------------------- */ +static int read_at(HANDLE f, unsigned long long off, void *buf, DWORD n) +{ + LARGE_INTEGER li; li.QuadPart = (LONGLONG)off; + DWORD got = 0; + return SetFilePointerEx(f, li, 0, FILE_BEGIN) && ReadFile(f, buf, n, &got, 0) && got == n; +} +/* a file in the root directory: its first sector and size */ +static int iso_find(HANDLE f, const unsigned char *root, unsigned rootn, const char *name, unsigned *lba, unsigned *size) +{ + size_t nl = strlen(name); + for (unsigned o = 0; o < rootn;) { + unsigned len = root[o]; + if (!len) { o = (o / 2048 + 1) * 2048; continue; } + unsigned fl = root[o + 32]; + const char *fn = (const char *)root + o + 33; + if (fl >= nl && !_strnicmp(fn, name, nl) && (fl == nl || fn[nl] == ';')) { + memcpy(lba, root + o + 2, 4); memcpy(size, root + o + 10, 4); + return 1; + } + o += len; + } + (void)f; + return 0; +} +static void identify(disc_t *d) +{ + d->game = -1; d->serial[0] = d->md5[0] = 0; + HANDLE f = CreateFileW(d->path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0); + if (f == INVALID_HANDLE_VALUE) { d->status = ST_MISSING; return; } + unsigned char pvd[2048]; + d->status = ST_NOTDISC; + if (!read_at(f, 16 * 2048ull, pvd, 2048) || memcmp(pvd + 1, "CD001", 5)) { CloseHandle(f); return; } + unsigned rlba, rsize; + memcpy(&rlba, pvd + 156 + 2, 4); memcpy(&rsize, pvd + 156 + 10, 4); + if (rsize > (1u << 20)) rsize = 1u << 20; + unsigned char *root = malloc(rsize); + if (!root || !read_at(f, rlba * 2048ull, root, rsize)) { free(root); CloseHandle(f); return; } + unsigned lba, size; + if (iso_find(f, root, rsize, "SYSTEM.CNF", &lba, &size) && size < 4096) { + char cnf[4096] = { 0 }; + if (read_at(f, lba * 2048ull, cnf, size)) { + char *b = strstr(cnf, "BOOT2"); + if (b && (b = strchr(b, '\\'))) { + int k = 0; + for (b++; *b && *b != ';' && *b != '\r' && *b != '\n' && k < 31; b++) d->serial[k++] = *b; + d->serial[k] = 0; + } + } + } + d->status = ST_UNKNOWN; + for (int i = 0; i < NGAMES; i++) if (!_stricmp(games[i].serial, d->serial)) d->game = i; + if (d->game >= 0 && iso_find(f, root, rsize, d->serial, &lba, &size) && size < (64u << 20)) { + unsigned char *exe = malloc(size); + if (exe && read_at(f, lba * 2048ull, exe, size)) { md5_t m; md5_init(&m); md5_add(&m, exe, size); md5_hex(&m, d->md5); } + free(exe); + } + free(root); + CloseHandle(f); + if (d->game < 0) return; + const game_t *g = &games[d->game]; + if (!g->exe) d->status = ST_NOTYET; + else if (g->md5 && strcmp(g->md5, d->md5)) d->status = ST_REVISION; + else d->status = ST_OK; +} + +/* ---- game folders --------------------------------------------------------------------- */ +static void game_dir(const game_t *g, wchar_t *out) +{ + /* next to this program, in its games\\ folder (built by the Build button), or in + * build\\windows (where development builds put it) */ + const wchar_t *tries[3] = { L"", g->dir, L"build\\windows" }; + for (int i = 0; i < 3; i++) { + wchar_t d[MAX_PATH], p[MAX_PATH]; + if (tries[i][0]) swprintf(d, MAX_PATH, L"%ls\\%ls", base_dir, tries[i]); else wcscpy(d, base_dir); + swprintf(p, MAX_PATH, L"%ls\\%ls", d, g->exe); + if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) { wcscpy(out, d); return; } + } + swprintf(out, MAX_PATH, L"%ls\\%ls", base_dir, g->dir); +} +static int game_installed(const game_t *g) +{ + wchar_t d[MAX_PATH], p[MAX_PATH]; + if (!g->exe) return 0; + game_dir(g, d); + swprintf(p, MAX_PATH, L"%ls\\%ls", d, g->exe); + if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) return 0; + swprintf(p, MAX_PATH, L"%ls\\rac1_game.dll", d); + if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return 1; + swprintf(p, MAX_PATH, L"%ls\\rac1_game.dll.manifest", d); /* still in parts: the wrapper joins them */ + return GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES; +} + +/* ---- settings and library on disk ------------------------------------------------------ */ +static int ini_int(const wchar_t *sec, const wchar_t *key, int def) { return (int)GetPrivateProfileIntW(sec, key, def, ini_path); } +static void ini_set_int(const wchar_t *sec, const wchar_t *key, int v) { wchar_t b[16]; swprintf(b, 16, L"%d", v); WritePrivateProfileStringW(sec, key, b, ini_path); } +static void save_all(void) +{ + WritePrivateProfileStringW(L"library", 0, 0, ini_path); + ini_set_int(L"library", L"count", ndiscs); + for (int i = 0; i < ndiscs; i++) { wchar_t k[16]; swprintf(k, 16, L"disc%d", i); WritePrivateProfileStringW(L"library", k, discs[i].path, ini_path); } + ini_set_int(L"library", L"selected", sel); + ini_set_int(L"settings", L"upscaled", set_upscale); + ini_set_int(L"settings", L"vsync", set_vsync); + ini_set_int(L"settings", L"no_gs_thread", set_nomtgs); + ini_set_int(L"settings", L"no_gl_thread", set_noglt); + ini_set_int(L"settings", L"fps_log", set_fpslog); + ini_set_int(L"settings", L"capture_seconds", set_capsec); + ini_set_int(L"settings", L"start_level", set_level); + ini_set_int(L"settings", L"minimize", set_minimize); +} +static int add_disc(const wchar_t *path, int select) +{ + wchar_t full[MAX_PATH]; + if (!GetFullPathNameW(path, MAX_PATH, full, 0)) wcscpy(full, path); + for (int i = 0; i < ndiscs; i++) if (!_wcsicmp(discs[i].path, full)) { if (select) sel = i; return i; } + if (ndiscs >= MAXDISCS) return -1; + disc_t *d = &discs[ndiscs]; + memset(d, 0, sizeof *d); + wcsncpy(d->path, full, MAX_PATH - 1); + identify(d); + if (select) sel = ndiscs; + return ndiscs++; +} +static void load_all(void) +{ + int n = ini_int(L"library", L"count", 0); + for (int i = 0; i < n && i < MAXDISCS; i++) { + wchar_t k[16], p[MAX_PATH]; + swprintf(k, 16, L"disc%d", i); + GetPrivateProfileStringW(L"library", k, L"", p, MAX_PATH, ini_path); + if (p[0]) add_disc(p, 0); + } + sel = ini_int(L"library", L"selected", ndiscs ? 0 : -1); + if (sel >= ndiscs) sel = ndiscs - 1; + set_upscale = ini_int(L"settings", L"upscaled", 1); + set_vsync = ini_int(L"settings", L"vsync", 0); + set_nomtgs = ini_int(L"settings", L"no_gs_thread", 0); + set_noglt = ini_int(L"settings", L"no_gl_thread", 0); + set_fpslog = ini_int(L"settings", L"fps_log", 0); + set_capsec = ini_int(L"settings", L"capture_seconds", 3); + set_level = ini_int(L"settings", L"start_level", -1); + set_minimize = ini_int(L"settings", L"minimize", 1); + if (set_capsec < 1 || set_capsec > 10) set_capsec = 3; +} + +/* ---- running a game ---------------------------------------------------------------------- */ +static HANDLE game_proc; +static int running = -1; +static wchar_t message[256]; /* a line under the Play button */ +static int message_bad; +#define WM_GAME_EXIT (WM_APP + 1) + +static DWORD WINAPI watch(LPVOID p) +{ + HANDLE h = (HANDLE)p; DWORD code = 0; + WaitForSingleObject(h, INFINITE); + GetExitCodeProcess(h, &code); + PostMessageW(hwnd, WM_GAME_EXIT, code, 0); + return 0; +} +static void env(const wchar_t *k, const wchar_t *v) { SetEnvironmentVariableW(k, v); } +/* Upscaled: the smallest whole multiple of the PS2's 448 lines that covers the height of + * the monitor this window is on (1080p: 3x, 1440p and 4K: 4x), at least 2x */ +static int screen_h(void) +{ + MONITORINFO mi; mi.cbSize = sizeof mi; + if (GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) return mi.rcMonitor.bottom - mi.rcMonitor.top; + return GetSystemMetrics(SM_CYSCREEN); +} +static int upscale_factor(void) +{ + int f = (screen_h() + 447) / 448; + return f < 2 ? 2 : f > 4 ? 4 : f; +} +static void play(int i) +{ + if (i < 0 || running >= 0) return; + disc_t *d = &discs[i]; + identify(d); + if (d->status != ST_OK && d->status != ST_REVISION) return; + const game_t *g = &games[d->game]; + wchar_t dir[MAX_PATH], exe[MAX_PATH], cmd[2 * MAX_PATH + 64]; + game_dir(g, dir); + swprintf(exe, MAX_PATH, L"%ls\\%ls", dir, g->exe); + if (!game_installed(g)) { + swprintf(message, 256, L"The game files are not in %ls", dir); message_bad = 1; + InvalidateRect(hwnd, 0, 0); return; + } + wchar_t b[32]; + env(L"UYA_RENDERER", L"gl"); + swprintf(b, 32, L"%d", set_upscale ? upscale_factor() : 1); env(L"UYA_SCALE", b); + env(L"UYA_VSYNC", set_vsync ? L"1" : 0); + env(L"UYA_MTGS", set_nomtgs ? L"0" : 0); + env(L"UYA_GLTHREAD", set_noglt ? L"0" : 0); + env(L"UYA_FPSLOG", set_fpslog ? L"1" : 0); + swprintf(b, 32, L"%d", set_capsec * 50); env(L"UYA_GSREC_N", b); + if (set_level >= 0) { swprintf(b, 32, L"%d@1", set_level); env(L"UYA_LEVEL", b); } else env(L"UYA_LEVEL", 0); + swprintf(cmd, sizeof cmd / sizeof cmd[0], L"\"%ls\" - \"%ls\"", exe, d->path); + STARTUPINFOW si; PROCESS_INFORMATION pi; + memset(&si, 0, sizeof si); si.cb = sizeof si; + if (!CreateProcessW(exe, cmd, 0, 0, FALSE, 0, 0, dir, &si, &pi)) { + swprintf(message, 256, L"Could not start %ls (error %lu)", g->exe, GetLastError()); message_bad = 1; + InvalidateRect(hwnd, 0, 0); return; + } + CloseHandle(pi.hThread); + game_proc = pi.hProcess; running = i; + message[0] = 0; message_bad = 0; + CloseHandle(CreateThread(0, 0, watch, pi.hProcess, 0, 0)); + if (set_minimize) ShowWindow(hwnd, SW_MINIMIZE); + InvalidateRect(hwnd, 0, 0); +} +#include "solana_build.h" + +static void open_log(int i) +{ + if (i < 0 || discs[i].game < 0 || !games[discs[i].game].exe) return; + wchar_t dir[MAX_PATH], p[MAX_PATH]; + const game_t *g = &games[discs[i].game]; + game_dir(g, dir); + swprintf(p, MAX_PATH, L"%ls\\rac1_log.txt", dir); + if (!game_installed(g) || (bld.failed && bld.disc == i)) swprintf(p, MAX_PATH, L"%ls\\games\\%ls\\build_log.txt", base_dir, g->kit); /* (the build's) */ + if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { wcscpy(message, L"No log yet: it is written when the game runs."); message_bad = 0; InvalidateRect(hwnd, 0, 0); return; } + ShellExecuteW(hwnd, L"open", p, 0, 0, SW_SHOWNORMAL); +} +static void open_folder(int i) +{ + if (i < 0 || discs[i].game < 0 || !games[discs[i].game].exe) return; + wchar_t dir[MAX_PATH]; + game_dir(&games[discs[i].game], dir); + ShellExecuteW(hwnd, L"open", dir, 0, 0, SW_SHOWNORMAL); +} + +/* ---- adding discs ---------------------------------------------------------------------------- */ +static void pick_files(void) +{ + static wchar_t buf[32768]; + OPENFILENAMEW of; memset(&of, 0, sizeof of); + buf[0] = 0; + of.lStructSize = sizeof of; of.hwndOwner = hwnd; + of.lpstrFilter = L"PS2 disc images (*.iso)\0*.iso\0All files\0*.*\0"; + of.lpstrFile = buf; of.nMaxFile = 32768; + of.lpstrTitle = L"Add disc images"; + of.Flags = OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ALLOWMULTISELECT | OFN_HIDEREADONLY; + if (!GetOpenFileNameW(&of)) return; + wchar_t *p = buf + wcslen(buf) + 1; + if (!*p) add_disc(buf, 1); + else for (; *p; p += wcslen(p) + 1) { wchar_t f[MAX_PATH]; swprintf(f, MAX_PATH, L"%ls\\%ls", buf, p); add_disc(f, 1); } + save_all(); +} +static int scan_dir(const wchar_t *dir, int depth) +{ + wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; int n = 0; + swprintf(pat, MAX_PATH, L"%ls\\*", dir); + HANDLE h = FindFirstFileW(pat, &fd); + if (h == INVALID_HANDLE_VALUE) return 0; + do { + if (fd.cFileName[0] == L'.') continue; + wchar_t p[MAX_PATH]; swprintf(p, MAX_PATH, L"%ls\\%ls", dir, fd.cFileName); + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (depth < 4) n += scan_dir(p, depth + 1); } + else { size_t l = wcslen(fd.cFileName); if (l > 4 && !_wcsicmp(fd.cFileName + l - 4, L".iso")) { int before = ndiscs; add_disc(p, 0); n += ndiscs > before; } } + } while (FindNextFileW(h, &fd)); + FindClose(h); + return n; +} +static void pick_folder(void) +{ + BROWSEINFOW bi; memset(&bi, 0, sizeof bi); + bi.hwndOwner = hwnd; bi.lpszTitle = L"Find disc images (.iso) in this folder and its subfolders"; + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + LPITEMIDLIST id = SHBrowseForFolderW(&bi); + if (!id) return; + wchar_t dir[MAX_PATH]; + if (SHGetPathFromIDListW(id, dir)) { + HCURSOR c = SetCursor(LoadCursor(0, IDC_WAIT)); + int n = scan_dir(dir, 0); + SetCursor(c); + swprintf(message, 256, n == 1 ? L"Found 1 new disc image." : L"Found %d new disc images.", n); message_bad = 0; + if (n && sel < 0) sel = 0; + save_all(); + } + CoTaskMemFree(id); + InvalidateRect(hwnd, 0, 0); +} + +/* ---- drawing ----------------------------------------------------------------------------------- */ +/* the palette (assets/palette.json, "Horizon") */ +#define C_BG RGB(0x0A, 0x0E, 0x1A) +#define C_SIDE RGB(0x0F, 0x14, 0x24) +#define C_CARD RGB(0x15, 0x1C, 0x30) +#define C_CARDHI RGB(0x1C, 0x25, 0x40) +#define C_SELECT RGB(0x1C, 0x25, 0x40) +#define C_LINE RGB(0x26, 0x30, 0x4D) +#define C_TEXT RGB(0xF4, 0xF6, 0xFB) +#define C_MUTED RGB(0x9A, 0xA6, 0xC6) +#define C_DIM RGB(0x5E, 0x68, 0x88) +#define C_ACCENT RGB(0xFF, 0x7A, 0x3D) +#define C_ACCENTHI RGB(0xFF, 0x94, 0x61) +#define C_ONACCENT RGB(0x0A, 0x0E, 0x1A) +#define C_CYAN RGB(0x7F, 0xD1, 0xFF) +#define C_OK RGB(0x3D, 0xD6, 0x8C) +#define C_WARN RGB(0xFF, 0xB2, 0x38) +#define C_BAD RGB(0xFF, 0x5A, 0x5F) +#include "solana_img.h" + +enum { W_NONE, W_ADD, W_SCAN, W_ITEM, W_PLAY, W_SETTINGS, W_LOG, W_FOLDER, W_REMOVE, W_BACK, + W_RENDER, W_TOGGLE, W_CAPDOWN, W_CAPUP, W_LVLDOWN, W_LVLUP, W_CANCEL, W_REBUILD }; +typedef struct { RECT r; int id, arg; } widget_t; +static widget_t wid[96]; +static int nwid, hover_id = W_NONE, hover_arg, press_id = W_NONE, press_arg; +static int view_settings, scroll; +static HFONT f_title, f_head, f_body, f_bold, f_small, f_logo, f_btn; + +static void add_w(RECT r, int id, int arg) { if (nwid < 96) { wid[nwid].r = r; wid[nwid].id = id; wid[nwid].arg = arg; nwid++; } } +static int is_hover(int id, int arg) { return hover_id == id && hover_arg == arg; } +static HFONT mkfont(int px, int weight) { return CreateFontW(-S(px), 0, 0, 0, weight, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI"); } +static void make_fonts(void) +{ + HFONT *all[] = { &f_title, &f_head, &f_body, &f_bold, &f_small, &f_logo, &f_btn }; + for (unsigned i = 0; i < sizeof all / sizeof all[0]; i++) if (*all[i]) DeleteObject(*all[i]); + f_title = mkfont(30, FW_SEMIBOLD); f_head = mkfont(15, FW_SEMIBOLD); f_body = mkfont(14, FW_NORMAL); + f_bold = mkfont(15, FW_SEMIBOLD); f_small = mkfont(12, FW_NORMAL); f_logo = mkfont(17, FW_BOLD); f_btn = mkfont(14, FW_SEMIBOLD); +} +static void fill(HDC dc, RECT r, COLORREF c) { SetDCBrushColor(dc, c); FillRect(dc, &r, GetStockObject(DC_BRUSH)); } +static void rrect(HDC dc, RECT r, int rad, COLORREF fillc, COLORREF edge) +{ + SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(DC_PEN)); + SetDCBrushColor(dc, fillc); SetDCPenColor(dc, edge); + RoundRect(dc, r.left, r.top, r.right, r.bottom, S(rad), S(rad)); +} +static void text(HDC dc, const wchar_t *s, RECT r, HFONT f, COLORREF c, UINT fmt) +{ + SelectObject(dc, f); SetTextColor(dc, c); SetBkMode(dc, TRANSPARENT); + DrawTextW(dc, s, -1, &r, fmt | DT_NOPREFIX); +} +static int text_w(HDC dc, const wchar_t *s, HFONT f) { SIZE z; SelectObject(dc, f); GetTextExtentPoint32W(dc, s, (int)wcslen(s), &z); return z.cx; } +static RECT rc(int l, int t, int r, int b) { RECT x = { l, t, r, b }; return x; } + +static void status_text(const disc_t *d, wchar_t *out, COLORREF *c) +{ + switch (d->status) { + case ST_OK: if (bld.active && bld.disc == (int)(d - discs)) { wcscpy(out, L"Building\u2026"); *c = C_ACCENT; } + else if (games[d->game].exe && !game_installed(&games[d->game])) { wcscpy(out, games[d->game].kit ? L"Ready to build" : L"Game files missing"); *c = games[d->game].kit ? C_ACCENT : C_BAD; } + else { wcscpy(out, L"Ready to play"); *c = C_OK; } break; + case ST_REVISION: wcscpy(out, L"Different disc revision"); *c = C_WARN; break; + case ST_NOTYET: wcscpy(out, L"Not supported yet"); *c = C_DIM; break; + case ST_UNKNOWN: wcscpy(out, L"Unknown PS2 disc"); *c = C_DIM; break; + case ST_MISSING: wcscpy(out, L"Disc image not found"); *c = C_BAD; break; + default: wcscpy(out, L"Not a PS2 disc image"); *c = C_BAD; break; + } +} +static void disc_title(const disc_t *d, wchar_t *out) +{ + if (d->game >= 0) { wcscpy(out, games[d->game].title); return; } + const wchar_t *n = wcsrchr(d->path, L'\\'); n = n ? n + 1 : d->path; + wcsncpy(out, n, 127); out[127] = 0; +} + +/* a button: label, and an icon (assets/png/icons, white, tinted here) left of it */ +static void button_i(HDC dc, RECT r, const wchar_t *label, int id, int arg, int primary, int enabled, int icon) +{ + COLORREF bg = primary ? C_ACCENT : C_CARD, fg = primary ? C_ONACCENT : C_TEXT, edge = primary ? C_ACCENT : C_LINE; + if (!enabled) { bg = primary ? RGB(0x5A, 0x3A, 0x2C) : C_CARD; fg = primary ? RGB(0x9A, 0x7E, 0x70) : C_DIM; edge = primary ? bg : C_LINE; } + else if (is_hover(id, arg)) { bg = primary ? C_ACCENTHI : C_CARDHI; edge = primary ? bg : RGB(0x34, 0x40, 0x66); } + if (enabled && press_id == id && press_arg == arg && is_hover(id, arg)) OffsetRect(&r, 0, S(1)); + rrect(dc, r, 6, bg, edge); + HFONT f = primary ? f_bold : f_btn; + int isz = S(18), gap = S(8), tw = text_w(dc, label, f); + int total = tw + (icon ? isz + gap : 0), x = (r.left + r.right - total) / 2; + if (icon) { img_icon(dc, icon, x, (r.top + r.bottom - isz) / 2, isz, fg); x += isz + gap; } + text(dc, label, rc(x, r.top, x + tw + S(2), r.bottom), f, fg, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + if (enabled) add_w(r, id, arg); +} +static void button(HDC dc, RECT r, const wchar_t *label, int id, int arg, int primary, int enabled) { button_i(dc, r, label, id, arg, primary, enabled, 0); } +static void toggle(HDC dc, int x, int y, int on, int arg) +{ + RECT r = rc(x, y, x + S(40), y + S(22)); + rrect(dc, r, 22, on ? C_ACCENT : C_LINE, on ? C_ACCENT : C_LINE); + int kx = on ? r.right - S(19) : r.left + S(3); + RECT k = rc(kx, y + S(3), kx + S(16), y + S(19)); + rrect(dc, k, 16, on ? C_ONACCENT : C_TEXT, on ? C_ONACCENT : C_TEXT); +} +/* one row of the settings list: title, explanation, and the control area on the right */ +static int setting_row(HDC dc, RECT area, int y, const wchar_t *title, const wchar_t *help) +{ + RECT t = rc(area.left, y, area.right - S(220), y + S(22)); + text(dc, title, t, f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER); + RECT h = rc(area.left, y + S(22), area.right - S(220), y + S(58)); + text(dc, help, h, f_small, C_MUTED, DT_LEFT | DT_WORDBREAK); + return y + S(66); +} +static void draw_settings(HDC dc, RECT area) +{ + int y = area.top; + text(dc, L"Settings", rc(area.left, y, area.right, y + S(40)), f_title, C_TEXT, DT_LEFT | DT_SINGLELINE); + button(dc, rc(area.right - S(90), y + S(4), area.right, y + S(36)), L"Done", W_BACK, 0, 0, 1); + y += S(52); + text(dc, L"Apply the next time a game starts.", rc(area.left, y, area.right, y + S(20)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE); + y += S(34); + int cx = area.right - S(200); + + int y0 = y; + { wchar_t help[200]; + swprintf(help, 200, L"Native draws at the PS2's own resolution, as the game was. Upscaled draws at %dx for this %d-line screen: sharper, more GPU work.", upscale_factor(), screen_h()); + y = setting_row(dc, area, y, L"Resolution", help); + } + { const wchar_t *opt[2] = { L"Native", L"Upscaled" }; + RECT box = rc(cx, y0 + S(4), area.right, y0 + S(36)); + rrect(dc, box, 6, C_CARD, C_LINE); + for (int k = 0; k < 2; k++) { + RECT s = rc(box.left + S(3) + k * (S(197) / 2), box.top + S(3), box.left + S(3) + (k + 1) * (S(197) / 2) - S(3), box.bottom - S(3)); + int on = (k == 1) == (set_upscale != 0); + if (on) rrect(dc, s, 6, C_ACCENT, C_ACCENT); else if (is_hover(W_RENDER, k)) rrect(dc, s, 6, C_CARDHI, C_CARDHI); + text(dc, opt[k], s, f_btn, on ? C_ONACCENT : C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + add_w(s, W_RENDER, k); + } + } + struct { const wchar_t *t, *h; int *v; int invert; } rows[] = { + { L"VSync", L"Waits for the monitor's refresh. Off lets the game pace itself at 50 fps.", &set_vsync, 0 }, + { L"Graphics on its own thread", L"Runs the PS2 graphics chip beside the game. Turn off only to test a problem.", &set_nomtgs, 1 }, + { L"OpenGL calls on their own thread", L"Sends drawing to the driver from a separate thread. Turn off only to test a problem.", &set_noglt, 1 }, + { L"Performance statistics", L"Writes frame timings to rac1_log.txt every 2 seconds, for bug reports.", &set_fpslog, 0 }, + { L"Minimize while playing", L"Tucks this window away when a game starts.", &set_minimize, 0 }, + }; + for (unsigned i = 0; i < sizeof rows / sizeof rows[0]; i++) { + int ry = y; + y = setting_row(dc, area, y, rows[i].t, rows[i].h); + int on = rows[i].invert ? !*rows[i].v : *rows[i].v; + RECT hit = rc(area.right - S(48), ry + S(4), area.right, ry + S(30)); + toggle(dc, area.right - S(44), ry + S(4), on, (int)i); + add_w(hit, W_TOGGLE, (int)i); + } + /* steppers */ + { int ry = y; wchar_t b[64]; + y = setting_row(dc, area, y, L"F9 capture length", L"F9 in the game records this much graphics input to gsrec_NN.bin, for glitch reports (about 260 MB a second)."); + swprintf(b, 64, L"%d second%ls", set_capsec, set_capsec == 1 ? L"" : L"s"); + RECT box = rc(cx, ry + S(4), area.right, ry + S(36)); + rrect(dc, box, 6, C_CARD, C_LINE); + text(dc, b, box, f_btn, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + RECT l = rc(box.left + S(3), box.top + S(3), box.left + S(35), box.bottom - S(3)), r = rc(box.right - S(35), box.top + S(3), box.right - S(3), box.bottom - S(3)); + if (is_hover(W_CAPDOWN, 0)) rrect(dc, l, 6, C_CARDHI, C_CARDHI); + if (is_hover(W_CAPUP, 0)) rrect(dc, r, 6, C_CARDHI, C_CARDHI); + text(dc, L"−", l, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + text(dc, L"+", r, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + add_w(l, W_CAPDOWN, 0); add_w(r, W_CAPUP, 0); + } + { int ry = y; wchar_t b[64]; + y = setting_row(dc, area, y, L"Start in a level (testing)", L"Skips the menus and loads this level with a fresh save. Off for normal play."); + if (set_level < 0) wcscpy(b, L"Off"); else swprintf(b, 64, L"%02d %ls", set_level, rac1_levels[set_level]); + RECT box = rc(cx, ry + S(4), area.right, ry + S(36)); + rrect(dc, box, 6, set_level >= 0 ? C_SELECT : C_CARD, set_level >= 0 ? C_ACCENT : C_LINE); + RECT mid = rc(box.left + S(36), box.top, box.right - S(36), box.bottom); + text(dc, b, mid, f_btn, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + RECT l = rc(box.left + S(3), box.top + S(3), box.left + S(35), box.bottom - S(3)), r = rc(box.right - S(35), box.top + S(3), box.right - S(3), box.bottom - S(3)); + if (is_hover(W_LVLDOWN, 0)) rrect(dc, l, 6, C_CARDHI, C_CARDHI); + if (is_hover(W_LVLUP, 0)) rrect(dc, r, 6, C_CARDHI, C_CARDHI); + text(dc, L"‹", l, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + text(dc, L"›", r, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + add_w(l, W_LVLDOWN, 0); add_w(r, W_LVLUP, 0); + } + { /* building the selected game again (a newer build kit, or after a problem) */ + int ry = y; + int ok = sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].kit && (discs[sel].status == ST_OK || discs[sel].status == ST_REVISION) && !bld.active && running < 0; + y = setting_row(dc, area, y, L"Rebuild the game", L"Downloads the latest build kit and builds the selected game from your disc again. Saves are kept."); + button_i(dc, rc(cx, ry + S(4), area.right, ry + S(36)), L"Rebuild", W_REBUILD, 0, 0, ok, IMG_I_REBUILD); + } +} + +static void info_row(HDC dc, int x, int *y, int w, const wchar_t *k, const wchar_t *v, COLORREF vc) +{ + text(dc, k, rc(x, *y, x + S(130), *y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_VCENTER); + text(dc, v, rc(x + S(130), *y, x + w, *y + S(22)), f_body, vc, DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_PATH_ELLIPSIS); + *y += S(30); +} +static void draw_details(HDC dc, RECT area) +{ + if (sel < 0 || sel >= ndiscs) { + int cy = (area.top + area.bottom) / 2 + S(70); + img_draw(dc, IMG_EMPTY, (area.left + area.right) / 2 - S(160), cy - S(290), S(320), S(200)); + text(dc, L"Your library is empty", rc(area.left, cy - S(60), area.right, cy - S(24)), f_title, C_TEXT, DT_CENTER | DT_SINGLELINE); + text(dc, L"Add your own copy of the game as a disc image (.iso), or drop one on this window.", rc(area.left, cy - S(14), area.right, cy + S(10)), f_body, C_MUTED, DT_CENTER | DT_SINGLELINE); + int bw = S(176); + button_i(dc, rc((area.left + area.right) / 2 - bw - S(6), cy + S(30), (area.left + area.right) / 2 - S(6), cy + S(70)), L"Add disc image", W_ADD, 1, 1, 1, IMG_I_ADD); + button_i(dc, rc((area.left + area.right) / 2 + S(6), cy + S(30), (area.left + area.right) / 2 + bw + S(6), cy + S(70)), L"Scan a folder", W_SCAN, 1, 0, 1, IMG_I_SCAN); + return; + } + disc_t *d = &discs[sel]; + const game_t *g = d->game >= 0 ? &games[d->game] : 0; + wchar_t t[160], st[64], b[MAX_PATH + 64]; COLORREF sc; + disc_title(d, t); status_text(d, st, &sc); + int y = area.top, w = area.right - area.left; + + /* banner */ + RECT ban = rc(area.left, y, area.right, y + S(150)); + { /* the Horizon banner (assets/png/default_banner.png): filling the area, anchored right, under rounded corners */ + HRGN rg = CreateRoundRectRgn(ban.left, ban.top, ban.right + 1, ban.bottom + 1, S(10), S(10)); + SelectClipRgn(dc, rg); + fill(dc, ban, C_BG); + img_cover(dc, IMG_BANNER, ban, 1.0); + SelectClipRgn(dc, 0); DeleteObject(rg); + } + text(dc, t, rc(ban.left + S(24), ban.top + S(30), ban.right - S(200), ban.top + S(74)), f_title, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + { int x = ban.left + S(24), by = ban.top + S(84); + if (g) { + int tw = text_w(dc, g->region, f_small) + S(16); + RECT chip = rc(x, by, x + tw, by + S(22)); + rrect(dc, chip, 22, C_CARD, C_LINE); + text(dc, g->region, chip, f_small, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + x += tw + S(10); + } + SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(NULL_PEN)); + SetDCBrushColor(dc, sc); Ellipse(dc, x, by + S(7), x + S(9), by + S(16)); + text(dc, st, rc(x + S(15), by, ban.right - S(200), by + S(22)), f_body, C_TEXT, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + } + y = ban.bottom + S(24); + + /* actions */ + int supported = (d->status == ST_OK || d->status == ST_REVISION) && g && g->exe; + int installed = supported && game_installed(g); + int building = bld.active && bld.disc == sel; + if (building) { /* the build's progress in place of the buttons */ + RECT pc = rc(area.left, y, area.right, y + S(48)); + int bw = (area.right - S(110)) - area.left; + RECT track = rc(area.left, y + S(30), area.left + bw - S(12), y + S(38)); + rrect(dc, track, 8, C_CARD, C_LINE); + int fw = (int)((track.right - track.left) * (bld.frac < 0 ? 0 : bld.frac > 1 ? 1 : bld.frac)); + if (fw > S(8)) rrect(dc, rc(track.left, track.top, track.left + fw, track.bottom), 8, C_ACCENT, C_ACCENT); + wchar_t pcnt[16]; swprintf(pcnt, 16, L"%d%%", (int)(bld.frac * 100 + 0.5)); + text(dc, bld.status, rc(area.left, y, area.left + bw - S(70), y + S(24)), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS); + text(dc, pcnt, rc(area.left + bw - S(70), y, area.left + bw - S(12), y + S(24)), f_bold, C_MUTED, DT_RIGHT | DT_SINGLELINE | DT_VCENTER); + button_i(dc, rc(area.right - S(110), y + S(4), area.right, y + S(44)), bld.cancel ? L"Stopping\u2026" : L"Cancel", W_CANCEL, 0, 0, !bld.cancel, IMG_I_CANCEL); + (void)pc; + } else { + int can = supported && running < 0 && (installed || (g->kit && !bld.active)); + const wchar_t *pl = running == sel ? L"Running\u2026" : (supported && !installed && g->kit) ? L"Build" : L"Play"; + button_i(dc, rc(area.left, y, area.left + S(170), y + S(48)), pl, W_PLAY, 0, 1, can, (supported && !installed && g->kit) ? IMG_I_BUILD : IMG_I_PLAY); + int bx = area.left + S(182); + int has_files = g && g->exe; + button_i(dc, rc(bx, y + S(4), bx + S(128), y + S(44)), L"Settings", W_SETTINGS, 0, 0, 1, IMG_I_SETTINGS); bx += S(140); + button_i(dc, rc(bx, y + S(4), bx + S(128), y + S(44)), L"Open log", W_LOG, 0, 0, has_files, IMG_I_LOG); bx += S(140); + button_i(dc, rc(bx, y + S(4), bx + S(150), y + S(44)), L"Game folder", W_FOLDER, 0, 0, has_files, IMG_I_FOLDER); + button_i(dc, rc(area.right - S(120), y + S(4), area.right, y + S(44)), L"Remove", W_REMOVE, 0, 0, running != sel && !(bld.active && bld.disc == sel), IMG_I_REMOVE); + } + y += S(60); + if (bld.failed && bld.disc == sel && !building) text(dc, bld.err, rc(area.left, y, area.right, y + S(22)), f_body, C_BAD, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + else if (message[0]) { text(dc, message, rc(area.left, y, area.right, y + S(22)), f_body, message_bad ? C_BAD : C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); } + else if (building) text(dc, L"Building from your disc. You can keep using your PC; closing this window stops the build.", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + else if (supported && !installed && g->kit) text(dc, L"First time: downloads about 95 MB of tools and source, then builds the game from your disc (10 to 30 minutes).", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + else if (d->status == ST_REVISION) text(dc, L"This disc's executable differs from the one this build was made from. It may not run correctly.", rc(area.left, y, area.right, y + S(22)), f_body, C_WARN, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + else if (d->status == ST_NOTYET) text(dc, L"This game is recognised, but its recompilation is not built yet.", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE); + y += S(36); + + /* the disc */ + RECT card = rc(area.left, y, area.right, y + S(190)); + rrect(dc, card, 10, C_CARD, C_LINE); + int x = card.left + S(20), iy = card.top + S(16), iw = w - S(40); + text(dc, L"Disc", rc(x, iy, x + iw, iy + S(22)), f_head, C_TEXT, DT_LEFT | DT_SINGLELINE); iy += S(32); + info_row(dc, x, &iy, iw, L"Image", d->path, C_TEXT); + MultiByteToWideChar(CP_ACP, 0, d->serial[0] ? d->serial : "none", -1, b, 64); + info_row(dc, x, &iy, iw, L"Boot executable", b, C_TEXT); + if (d->md5[0]) { + wchar_t m[64]; MultiByteToWideChar(CP_ACP, 0, d->md5, -1, m, 64); + swprintf(b, MAX_PATH + 64, L"%ls %ls", m, d->status == ST_OK ? L"(matches)" : d->status == ST_REVISION ? L"(different revision)" : L""); + info_row(dc, x, &iy, iw, L"MD5", b, d->status == ST_REVISION ? C_WARN : C_TEXT); + } else info_row(dc, x, &iy, iw, L"MD5", L"–", C_DIM); + if (g && g->exe) { + wchar_t gd[MAX_PATH]; game_dir(g, gd); + if (game_installed(g)) info_row(dc, x, &iy, iw, L"Game files", gd, C_TEXT); + else info_row(dc, x, &iy, iw, L"Game files", g->kit ? L"Not built yet" : L"Missing", g->kit ? C_MUTED : C_BAD); + } + else info_row(dc, x, &iy, iw, L"Game files", L"–", C_DIM); + y = card.bottom + S(12); + + /* the controller: the PS2's buttons and the same buttons on an Xbox pad (assets/png/controller_colour) */ + if (g && g->exe && y + S(232) < area.bottom) { + RECT c2 = rc(area.left, y, area.right, y + S(232)); + rrect(dc, c2, 10, C_CARD, C_LINE); + x = c2.left + S(20); iy = c2.top + S(16); + text(dc, L"Controller", rc(x, iy, x + S(200), iy + S(22)), f_head, C_TEXT, DT_LEFT | DT_SINGLELINE); + text(dc, L"Xbox, DualShock 4 and DualSense, over USB or Bluetooth", rc(x + S(110), iy, x + iw, iy + S(22)), f_small, C_MUTED, DT_RIGHT | DT_SINGLELINE | DT_VCENTER); + iy += S(36); + static const struct { int ps, xb; const wchar_t *pn, *xn; } rows[10] = { + { IMG_PS_CROSS, IMG_XB_A, L"Cross", L"A" }, { IMG_PS_CIRCLE, IMG_XB_B, L"Circle", L"B" }, { IMG_PS_SQUARE, IMG_XB_X, L"Square", L"X" }, + { IMG_PS_TRIANGLE, IMG_XB_Y, L"Triangle", L"Y" }, { IMG_PS_START, IMG_XB_MENU, L"Start", L"Menu" }, + { IMG_PS_L1, IMG_XB_LB, L"L1", L"LB" }, { IMG_PS_R1, IMG_XB_RB, L"R1", L"RB" }, { IMG_PS_L2, IMG_XB_LT, L"L2", L"LT" }, + { IMG_PS_R2, IMG_XB_RT, L"R2", L"RT" }, { IMG_PS_SELECT, IMG_XB_VIEW, L"Select", L"View" } }; + int col = iw / 2, gs = S(24), rh = S(30); + for (int i = 0; i < 10; i++) { + int cx0 = x + (i / 5) * col, ry = iy + (i % 5) * rh; + img_draw(dc, rows[i].ps, cx0, ry, gs, gs); + text(dc, rows[i].pn, rc(cx0 + gs + S(10), ry, cx0 + gs + S(100), ry + gs), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_VCENTER); + text(dc, L"\u2192", rc(cx0 + S(120), ry, cx0 + S(144), ry + gs), f_body, C_DIM, DT_CENTER | DT_SINGLELINE | DT_VCENTER); + img_draw(dc, rows[i].xb, cx0 + S(154), ry, gs, gs); + text(dc, rows[i].xn, rc(cx0 + S(154) + gs + S(10), ry, cx0 + col - S(8), ry + gs), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER); + } + } +} + +static void paint(HDC dc, RECT cr) +{ + nwid = 0; + { /* the starfield tile (512 x 512 at 3x: 171 px at 100%) behind the page */ + HBRUSH tb = img_tile_brush(IMG_BGTILE, S(171)); + if (tb) { SetBrushOrgEx(dc, 0, 0, 0); FillRect(dc, &cr, tb); } else fill(dc, cr, C_BG); + } + int sw = S(300), hh = S(64); + /* sidebar */ + RECT side = rc(0, 0, sw, cr.bottom); + fill(dc, side, C_SIDE); + fill(dc, rc(sw - 1, 0, sw, cr.bottom), C_LINE); + /* the wordmark (assets/png/wordmark.png, 260 x 40 at 100%) */ + if (!img_draw(dc, IMG_WORDMARK, S(20), S(14), S(260), S(40))) + text(dc, L"PROJECT SOLANA", rc(S(20), S(18), sw - S(10), S(46)), f_logo, C_TEXT, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + text(dc, L"LIBRARY", rc(S(20), hh + S(6), sw - S(20), hh + S(26)), f_small, C_DIM, DT_LEFT | DT_SINGLELINE); + int ly = hh + S(32) - scroll, ih = S(68); + HRGN clip = CreateRectRgn(0, hh + S(30), sw - 1, cr.bottom - S(116)); + SelectClipRgn(dc, clip); + for (int i = 0; i < ndiscs; i++, ly += ih + S(6)) { + RECT it = rc(S(12), ly, sw - S(12), ly + ih); + if (it.bottom < hh || it.top > cr.bottom) continue; + disc_t *d = &discs[i]; + COLORREF bg = i == sel ? C_SELECT : is_hover(W_ITEM, i) ? C_CARD : C_SIDE; + rrect(dc, it, 10, bg, i == sel ? C_LINE : bg); + if (i == sel) fill(dc, rc(it.left, it.top + S(14), it.left + S(3), it.bottom - S(14)), C_ACCENT); + wchar_t t[160], st[64], sub[128]; COLORREF sc; + disc_title(d, t); status_text(d, st, &sc); + text(dc, t, rc(it.left + S(16), it.top + S(10), it.right - S(12), it.top + S(32)), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + if (d->game >= 0) swprintf(sub, 128, L"%ls", games[d->game].region); else sub[0] = 0; + int x = it.left + S(16); + if (sub[0]) { text(dc, sub, rc(x, it.top + S(36), it.right, it.top + S(56)), f_small, C_MUTED, DT_LEFT | DT_SINGLELINE); x += text_w(dc, sub, f_small) + S(10); } + SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(NULL_PEN)); + SetDCBrushColor(dc, sc); Ellipse(dc, x, it.top + S(42), x + S(7), it.top + S(49)); + text(dc, st, rc(x + S(12), it.top + S(36), it.right - S(12), it.top + S(56)), f_small, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); + add_w(it, W_ITEM, i); + } + SelectClipRgn(dc, 0); DeleteObject(clip); + if (!ndiscs) text(dc, L"No games yet", rc(S(20), hh + S(36), sw - S(20), hh + S(60)), f_body, C_DIM, DT_LEFT | DT_SINGLELINE); + fill(dc, rc(0, cr.bottom - S(116), sw - 1, cr.bottom - S(115)), C_LINE); + button_i(dc, rc(S(16), cr.bottom - S(100), sw - S(16), cr.bottom - S(60)), L"Add disc image", W_ADD, 0, 0, 1, IMG_I_ADD); + button_i(dc, rc(S(16), cr.bottom - S(52), sw - S(16), cr.bottom - S(16)), L"Scan a folder", W_SCAN, 0, 0, 1, IMG_I_SCAN); + + RECT area = rc(sw + S(24), S(24), cr.right - S(24), cr.bottom - S(24)); + if (view_settings) draw_settings(dc, area); else draw_details(dc, area); +} + +/* ---- input ---------------------------------------------------------------------------------- */ +static widget_t *hit(int x, int y) +{ + POINT p = { x, y }; + for (int i = nwid - 1; i >= 0; i--) if (PtInRect(&wid[i].r, p)) return &wid[i]; + return 0; +} +static void click(int id, int arg) +{ + switch (id) { + case W_ADD: pick_files(); view_settings = 0; break; + case W_SCAN: pick_folder(); view_settings = 0; break; + case W_ITEM: sel = arg; view_settings = 0; message[0] = 0; identify(&discs[sel]); save_all(); break; + case W_PLAY: + if (sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].exe && !game_installed(&games[discs[sel].game])) build_start(sel); + else play(sel); + break; + case W_CANCEL: bld.cancel = 1; if (bld.job) TerminateJobObject(bld.job, 1); break; + case W_REBUILD: if (sel >= 0 && running < 0) { view_settings = 0; build_start(sel); } break; + case W_SETTINGS: view_settings = 1; break; + case W_BACK: view_settings = 0; save_all(); break; + case W_LOG: open_log(sel); break; + case W_FOLDER: open_folder(sel); break; + case W_REMOVE: + if (sel >= 0 && running != sel) { + memmove(&discs[sel], &discs[sel + 1], (size_t)(ndiscs - sel - 1) * sizeof discs[0]); + ndiscs--; + if (running > sel) running--; + if (sel >= ndiscs) sel = ndiscs - 1; + message[0] = 0; save_all(); + } + break; + case W_RENDER: set_upscale = arg; save_all(); break; + case W_TOGGLE: { + int *v[] = { &set_vsync, &set_nomtgs, &set_noglt, &set_fpslog, &set_minimize }; + *v[arg] = !*v[arg]; save_all(); break; } + case W_CAPDOWN: if (set_capsec > 1) set_capsec--; save_all(); break; + case W_CAPUP: if (set_capsec < 10) set_capsec++; save_all(); break; + case W_LVLDOWN: set_level = set_level < 0 ? 18 : set_level - 1; save_all(); break; + case W_LVLUP: set_level = set_level >= 18 ? -1 : set_level + 1; save_all(); break; + } + InvalidateRect(hwnd, 0, 0); +} + +static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + switch (m) { + case WM_PAINT: { + PAINTSTRUCT ps; HDC dc = BeginPaint(h, &ps); + RECT cr; GetClientRect(h, &cr); + HDC mem = CreateCompatibleDC(dc); + HBITMAP bm = CreateCompatibleBitmap(dc, cr.right, cr.bottom); + HGDIOBJ old = SelectObject(mem, bm); + paint(mem, cr); + BitBlt(dc, 0, 0, cr.right, cr.bottom, mem, 0, 0, SRCCOPY); + SelectObject(mem, old); DeleteObject(bm); DeleteDC(mem); + EndPaint(h, &ps); + return 0; } + case WM_ERASEBKGND: return 1; + case WM_MOUSEMOVE: { + widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); + int id = x ? x->id : W_NONE, arg = x ? x->arg : 0; + if (id != hover_id || arg != hover_arg) { hover_id = id; hover_arg = arg; InvalidateRect(h, 0, 0); } + SetCursor(LoadCursor(0, id != W_NONE ? IDC_HAND : IDC_ARROW)); + TRACKMOUSEEVENT t = { sizeof t, TME_LEAVE, h, 0 }; TrackMouseEvent(&t); + return 0; } + case WM_MOUSELEAVE: hover_id = W_NONE; InvalidateRect(h, 0, 0); return 0; + case WM_SETCURSOR: if (LOWORD(l) == HTCLIENT) return 1; break; + case WM_LBUTTONDOWN: { + widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); + press_id = x ? x->id : W_NONE; press_arg = x ? x->arg : 0; + SetCapture(h); InvalidateRect(h, 0, 0); + return 0; } + case WM_LBUTTONUP: { + widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); + int id = press_id, arg = press_arg; + press_id = W_NONE; ReleaseCapture(); + if (x && x->id == id && x->arg == arg) click(id, arg); else InvalidateRect(h, 0, 0); + return 0; } + case WM_LBUTTONDBLCLK: { + widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); + if (x && x->id == W_ITEM) { sel = x->arg; if (discs[sel].game >= 0 && games[discs[sel].game].exe && game_installed(&games[discs[sel].game])) play(sel); else InvalidateRect(h, 0, 0); } + return 0; } + case WM_MOUSEWHEEL: { + RECT cr; GetClientRect(h, &cr); + int total = ndiscs * (S(68) + S(6)), view = cr.bottom - S(116) - S(96); + scroll -= GET_WHEEL_DELTA_WPARAM(w) / 120 * S(60); + if (scroll > total - view) scroll = total - view; + if (scroll < 0) scroll = 0; + InvalidateRect(h, 0, 0); + return 0; } + case WM_KEYDOWN: + if (w == VK_RETURN && sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].exe && game_installed(&games[discs[sel].game])) play(sel); + else if ((w == VK_DOWN || w == VK_UP) && ndiscs) { sel += w == VK_DOWN ? 1 : -1; if (sel < 0) sel = 0; if (sel >= ndiscs) sel = ndiscs - 1; view_settings = 0; message[0] = 0; save_all(); InvalidateRect(h, 0, 0); } + else if (w == VK_ESCAPE && view_settings) { view_settings = 0; InvalidateRect(h, 0, 0); } + return 0; + case WM_DROPFILES: { + HDROP d = (HDROP)w; wchar_t p[MAX_PATH]; + UINT n = DragQueryFileW(d, 0xFFFFFFFF, 0, 0); + for (UINT i = 0; i < n; i++) if (DragQueryFileW(d, i, p, MAX_PATH)) { + if (GetFileAttributesW(p) & FILE_ATTRIBUTE_DIRECTORY) scan_dir(p, 0); else add_disc(p, 1); + } + DragFinish(d); + view_settings = 0; save_all(); InvalidateRect(h, 0, 0); + return 0; } + case WM_BUILD_UPDATE: + if (w == 1) { /* finished */ + if (!bld.failed) { wcscpy(message, L"Built. Ready to play."); message_bad = 0; FlashWindow(h, TRUE); } + if (IsIconic(h)) ShowWindow(h, SW_RESTORE); + } + InvalidateRect(h, 0, 0); + return 0; + case WM_CLOSE: + if (bld.active && MessageBoxW(h, L"A game is being built. Stop the build and close?", L"Project Solana", MB_YESNO | MB_ICONQUESTION) != IDYES) return 0; + DestroyWindow(h); + return 0; + case WM_GAME_EXIT: + if (game_proc) { CloseHandle(game_proc); game_proc = 0; } + if (w) { swprintf(message, 256, L"The game closed with an error (code 0x%lX). Open log shows what happened.", (unsigned long)w); message_bad = 1; } + running = -1; + if (IsIconic(h)) ShowWindow(h, SW_RESTORE); + InvalidateRect(h, 0, 0); + return 0; + case WM_GETMINMAXINFO: { MINMAXINFO *mi = (MINMAXINFO *)l; mi->ptMinTrackSize.x = S(940); mi->ptMinTrackSize.y = S(800); return 0; } + case WM_DPICHANGED: { + dpi = HIWORD(w) / 96.0f; make_fonts(); img_flush(); + RECT *r = (RECT *)l; SetWindowPos(h, 0, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER | SWP_NOACTIVATE); + return 0; } + case WM_SIZE: InvalidateRect(h, 0, 0); return 0; + case WM_DESTROY: save_all(); PostQuitMessage(0); return 0; + } + return DefWindowProcW(h, m, w, l); +} + +int WINAPI wWinMain(HINSTANCE inst, HINSTANCE prev, PWSTR cmdline, int show) +{ + (void)prev; (void)cmdline; + { /* per-monitor DPI when Windows has it (10 1703+), else system DPI */ + typedef BOOL (WINAPI *ctx_t)(HANDLE); + ctx_t f = (ctx_t)(void *)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext"); + if (!f || !f((HANDLE)-4)) SetProcessDPIAware(); + } + CoInitializeEx(0, COINIT_APARTMENTTHREADED); + GetModuleFileNameW(0, base_dir, MAX_PATH); + wchar_t *s = wcsrchr(base_dir, L'\\'); if (s) *s = 0; + swprintf(ini_path, MAX_PATH, L"%ls\\solana.ini", base_dir); + { HDC dc = GetDC(0); dpi = GetDeviceCaps(dc, LOGPIXELSX) / 96.0f; ReleaseDC(0, dc); } + make_fonts(); + load_all(); + + WNDCLASSEXW wc; memset(&wc, 0, sizeof wc); + wc.cbSize = sizeof wc; wc.style = CS_DBLCLKS; wc.lpfnWndProc = wndproc; wc.hInstance = inst; + wc.hIcon = LoadIconW(inst, MAKEINTRESOURCEW(1)); wc.hIconSm = wc.hIcon; + wc.hCursor = LoadCursor(0, IDC_ARROW); wc.lpszClassName = L"ProjectSolanaLauncher"; + RegisterClassExW(&wc); + hwnd = CreateWindowExW(WS_EX_ACCEPTFILES, wc.lpszClassName, L"Project Solana", WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, S(1100), S(860), 0, 0, inst, 0); + { /* the window's DPI may differ from the primary monitor's */ + typedef UINT (WINAPI *gdw_t)(HWND); + gdw_t f = (gdw_t)(void *)GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); + if (f) { float d2 = f(hwnd) / 96.0f; if (d2 != dpi && d2 > 0) { dpi = d2; make_fonts(); SetWindowPos(hwnd, 0, 0, 0, S(1100), S(860), SWP_NOMOVE | SWP_NOZORDER); } } + } + { /* dark title bar (Windows 10 2004+ / 11) */ + HMODULE dw = LoadLibraryW(L"dwmapi.dll"); + if (dw) { + typedef HRESULT (WINAPI *set_t)(HWND, DWORD, LPCVOID, DWORD); + set_t f = (set_t)(void *)GetProcAddress(dw, "DwmSetWindowAttribute"); + BOOL on = TRUE; if (f) { f(hwnd, 20, &on, sizeof on); COLORREF cap = C_SIDE; f(hwnd, 35, &cap, sizeof cap); } + } + } + ShowWindow(hwnd, show); + UpdateWindow(hwnd); + MSG msg; + while (GetMessageW(&msg, 0, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageW(&msg); } + return 0; +} diff --git a/launcher/solana.ico b/launcher/solana.ico new file mode 100644 index 0000000..a994755 Binary files /dev/null and b/launcher/solana.ico differ diff --git a/launcher/solana.manifest b/launcher/solana.manifest new file mode 100644 index 0000000..fd9f630 --- /dev/null +++ b/launcher/solana.manifest @@ -0,0 +1,10 @@ + + + + Project Solana launcher + + + true/pm + PerMonitorV2 + + diff --git a/launcher/solana.rc b/launcher/solana.rc new file mode 100644 index 0000000..7e7876e --- /dev/null +++ b/launcher/solana.rc @@ -0,0 +1,38 @@ +1 ICON "solana.ico" +1 24 "solana.manifest" + +100 RCDATA "assets/png/default_banner.png" +101 RCDATA "assets/png/wordmark.png" +102 RCDATA "assets/png/wordmark_text_only.png" +103 RCDATA "assets/png/empty_library.png" +104 RCDATA "assets/png/window_background_tile.png" +120 RCDATA "assets/png/icons/add.png" +121 RCDATA "assets/png/icons/build.png" +122 RCDATA "assets/png/icons/cancel.png" +123 RCDATA "assets/png/icons/game_folder.png" +124 RCDATA "assets/png/icons/open_log.png" +125 RCDATA "assets/png/icons/play.png" +126 RCDATA "assets/png/icons/rebuild.png" +127 RCDATA "assets/png/icons/remove.png" +128 RCDATA "assets/png/icons/scan.png" +129 RCDATA "assets/png/icons/settings.png" +140 RCDATA "assets/png/controller_colour/ps_cross.png" +141 RCDATA "assets/png/controller_colour/ps_circle.png" +142 RCDATA "assets/png/controller_colour/ps_square.png" +143 RCDATA "assets/png/controller_colour/ps_triangle.png" +144 RCDATA "assets/png/controller_colour/ps_l1.png" +145 RCDATA "assets/png/controller_colour/ps_r1.png" +146 RCDATA "assets/png/controller_colour/ps_l2.png" +147 RCDATA "assets/png/controller_colour/ps_r2.png" +148 RCDATA "assets/png/controller_colour/ps_start.png" +149 RCDATA "assets/png/controller_colour/ps_select.png" +160 RCDATA "assets/png/controller_colour/xbox_a.png" +161 RCDATA "assets/png/controller_colour/xbox_b.png" +162 RCDATA "assets/png/controller_colour/xbox_x.png" +163 RCDATA "assets/png/controller_colour/xbox_y.png" +164 RCDATA "assets/png/controller_colour/xbox_lb.png" +165 RCDATA "assets/png/controller_colour/xbox_rb.png" +166 RCDATA "assets/png/controller_colour/xbox_lt.png" +167 RCDATA "assets/png/controller_colour/xbox_rt.png" +168 RCDATA "assets/png/controller_colour/xbox_menu.png" +169 RCDATA "assets/png/controller_colour/xbox_view.png" diff --git a/launcher/solana_build.h b/launcher/solana_build.h new file mode 100644 index 0000000..3d81b90 --- /dev/null +++ b/launcher/solana_build.h @@ -0,0 +1,548 @@ +/* solana_build.h: the Build button (included once by solana.c). + * + * A game is built on the player's own PC from their own disc: nothing from the + * game is downloaded. From the game's GitHub release (RELEASE_URL + the game's + * tag, e.g. .../releases/download/rac1/) it fetches: + * .json what to fetch: version, files, sizes, SHA-256s + * toolchain-win64.zip Python and zig (the compiler); shared by all games, + * kept in toolchain\ and fetched again only when its version changes + * -build.zip the game's build kit: build.py, the recompilers, the + * host engine and the decomp's C, unpacked to games\\ + * then runs build.py on the disc image and keeps only what the game needs + * (games\\build\windows). The zips are checked against their SHA-256s and + * unpacked by the small inflate below (no tar.exe needed). + * + * solana.ini [build] release_url= and levels= override the + * source and the levels (testing, offline installs). */ +#include +#include + +#define RELEASE_URL L"https://github.com/OpenRAC/project-solana/releases/download/" +#define WM_BUILD_UPDATE (WM_APP + 2) + +typedef struct { + volatile int active, cancel, failed, disc; + volatile double frac; /* 0..1 for the bar */ + wchar_t status[256]; /* what it is doing */ + wchar_t err[512]; /* why it stopped */ + HANDLE job; /* the build's processes (closed: they end) */ +} build_t; +static build_t bld = { 0, 0, 0, -1, 0, {0}, {0}, 0 }; + +static void bld_post(void) { static DWORD last; DWORD t = GetTickCount(); if (t - last > 80) { last = t; PostMessageW(hwnd, WM_BUILD_UPDATE, 0, 0); } } +static void bld_status(double frac, const wchar_t *fmt, ...) +{ + va_list ap; va_start(ap, fmt); + _vsnwprintf(bld.status, 255, fmt, ap); bld.status[255] = 0; + va_end(ap); + if (frac >= 0) bld.frac = frac; + bld_post(); +} +static int bld_fail(const wchar_t *fmt, ...) +{ + va_list ap; va_start(ap, fmt); + _vsnwprintf(bld.err, 511, fmt, ap); bld.err[511] = 0; + va_end(ap); + bld.failed = 1; + return 0; +} + +/* ---- paths ---- */ +static void path_join(wchar_t *out, const wchar_t *a, const wchar_t *b) { swprintf(out, MAX_PATH, L"%ls\\%ls", a, b); } +static void mkdirs(const wchar_t *dir) +{ + wchar_t p[MAX_PATH]; wcsncpy(p, dir, MAX_PATH - 1); p[MAX_PATH - 1] = 0; + for (wchar_t *s = p + 3; *s; s++) if (*s == L'\\' || *s == L'/') { wchar_t c = *s; *s = 0; CreateDirectoryW(p, 0); *s = c; } + CreateDirectoryW(p, 0); +} +static void rm_tree(const wchar_t *path) +{ + wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; + DWORD a = GetFileAttributesW(path); + if (a == INVALID_FILE_ATTRIBUTES) return; + if (!(a & FILE_ATTRIBUTE_DIRECTORY)) { SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL); DeleteFileW(path); return; } + swprintf(pat, MAX_PATH, L"%ls\\*", path); + HANDLE h = FindFirstFileW(pat, &fd); + if (h != INVALID_HANDLE_VALUE) { + do { + if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue; + wchar_t c[MAX_PATH]; path_join(c, path, fd.cFileName); rm_tree(c); + } while (FindNextFileW(h, &fd)); + FindClose(h); + } + RemoveDirectoryW(path); +} + +/* ---- fetching: https (WinHTTP, redirects followed) or a local folder ---- */ +/* to a file (path) or to memory (*mem, *memn); progress from f0 to f1 of the bar */ +static int fetch(const wchar_t *url, const wchar_t *path, char **mem, size_t *memn, double f0, double f1, const wchar_t *what) +{ + HANDLE out = INVALID_HANDLE_VALUE; + size_t cap = 0, got = 0; char *buf = 0; + if (path) { out = CreateFileW(path, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (out == INVALID_HANDLE_VALUE) return bld_fail(L"Could not write %ls.", path); } + int ok = 0; + if (_wcsnicmp(url, L"http", 4)) { /* a folder */ + HANDLE in = CreateFileW(url, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (in == INVALID_HANDLE_VALUE) { bld_fail(L"%ls is not there.", url); goto end; } + LARGE_INTEGER sz; GetFileSizeEx(in, &sz); + static char chunk[1 << 20]; DWORD n; + while (ReadFile(in, chunk, sizeof chunk, &n, 0) && n) { + if (bld.cancel) { CloseHandle(in); goto end; } + if (path) { DWORD w; WriteFile(out, chunk, n, &w, 0); } + else { if (got + n + 1 > cap) { cap = (got + n + 1) * 2; buf = realloc(buf, cap); } memcpy(buf + got, chunk, n); } + got += n; + if (sz.QuadPart) bld_status(f0 + (f1 - f0) * (double)got / (double)sz.QuadPart, L"%ls (%.0f of %.0f MB)", what, got / 1048576.0, sz.QuadPart / 1048576.0); + } + CloseHandle(in); + ok = 1; + goto end; + } + { + URL_COMPONENTSW uc; wchar_t host[256], upath[2048]; + memset(&uc, 0, sizeof uc); uc.dwStructSize = sizeof uc; + uc.lpszHostName = host; uc.dwHostNameLength = 256; uc.lpszUrlPath = upath; uc.dwUrlPathLength = 2048; + if (!WinHttpCrackUrl(url, 0, 0, &uc)) { bld_fail(L"Bad address: %ls", url); goto end; } + HINTERNET s = WinHttpOpen(L"ProjectSolana/1.0", 4 /* WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY (8.1+) */, 0, 0, 0); + if (!s) s = WinHttpOpen(L"ProjectSolana/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, 0, 0, 0); + HINTERNET c = s ? WinHttpConnect(s, host, uc.nPort, 0) : 0; + HINTERNET r = c ? WinHttpOpenRequest(c, L"GET", upath, 0, 0, 0, uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0) : 0; + DWORD status = 0, sl = sizeof status; unsigned long long total = 0; + if (!r || !WinHttpSendRequest(r, 0, 0, 0, 0, 0, 0) || !WinHttpReceiveResponse(r, 0)) { + bld_fail(L"Could not download %ls (error %lu). Check your internet connection.", what, GetLastError()); + } else { + WinHttpQueryHeaders(r, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, 0, &status, &sl, 0); + wchar_t lenb[32]; DWORD ll = sizeof lenb; + if (WinHttpQueryHeaders(r, WINHTTP_QUERY_CONTENT_LENGTH, 0, lenb, &ll, 0)) total = _wcstoui64(lenb, 0, 10); + if (status != 200) bld_fail(L"Could not download %ls (the server answered %lu).", what, status); + else { + static char chunk[1 << 16]; + for (;;) { + DWORD n = 0; + if (bld.cancel) break; + if (!WinHttpReadData(r, chunk, sizeof chunk, &n)) { bld_fail(L"The download of %ls stopped (error %lu).", what, GetLastError()); break; } + if (!n) { ok = 1; break; } + if (path) { DWORD w; if (!WriteFile(out, chunk, n, &w, 0) || w != n) { bld_fail(L"Could not write %ls (disk full?).", path); break; } } + else { if (got + n + 1 > cap) { cap = (got + n + 1) * 2; buf = realloc(buf, cap); } memcpy(buf + got, chunk, n); } + got += n; + if (total) bld_status(f0 + (f1 - f0) * (double)got / (double)total, L"%ls (%.0f of %.0f MB)", what, got / 1048576.0, total / 1048576.0); + else bld_status(-1, L"%ls (%.0f MB)", what, got / 1048576.0); + } + } + } + if (r) WinHttpCloseHandle(r); + if (c) WinHttpCloseHandle(c); + if (s) WinHttpCloseHandle(s); + } +end: + if (out != INVALID_HANDLE_VALUE) CloseHandle(out); + if (!ok || bld.cancel) { free(buf); if (path) DeleteFileW(path); return 0; } + if (mem) { if (!buf) buf = malloc(1); buf[got] = 0; *mem = buf; *memn = got; } else free(buf); + return 1; +} + +/* ---- SHA-256 of a file (CNG) ---- */ +static int sha256_file(const wchar_t *path, char hex[65]) +{ + BCRYPT_ALG_HANDLE alg; BCRYPT_HASH_HANDLE h; unsigned char d[32]; int ok = 0; + if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, 0, 0)) return 0; + if (!BCryptCreateHash(alg, &h, 0, 0, 0, 0, 0)) { + HANDLE f = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0); + if (f != INVALID_HANDLE_VALUE) { + static unsigned char b[1 << 20]; DWORD n; + while (ReadFile(f, b, sizeof b, &n, 0) && n) BCryptHashData(h, b, n, 0); + CloseHandle(f); + if (!BCryptFinishHash(h, d, 32, 0)) { for (int i = 0; i < 32; i++) sprintf(hex + 2 * i, "%02x", d[i]); hex[64] = 0; ok = 1; } + } + BCryptDestroyHash(h); + } + BCryptCloseAlgorithmProvider(alg, 0); + return ok; +} + +/* ---- unzip: stored and deflated entries (RFC 1951 inflate) ---- */ +typedef struct { const unsigned char *in; size_t inn, ip; unsigned bb; int bc; unsigned char *out; size_t outn, op; int err; } inf_t; +typedef struct { short count[16], symbol[320]; } huff_t; +static int ibits(inf_t *s, int n) +{ + unsigned v = s->bb; + while (s->bc < n) { if (s->ip >= s->inn) { s->err = 1; return 0; } v |= (unsigned)s->in[s->ip++] << s->bc; s->bc += 8; } + s->bb = v >> n; s->bc -= n; + return (int)(v & ((1u << n) - 1)); +} +static int hdecode(inf_t *s, const huff_t *h) +{ + int code = 0, first = 0, index = 0; + for (int len = 1; len < 16; len++) { + code |= ibits(s, 1); + int count = h->count[len]; + if (code - count < first) return h->symbol[index + (code - first)]; + index += count; first += count; first <<= 1; code <<= 1; + if (s->err) return -1; + } + s->err = 1; return -1; +} +static int hbuild(huff_t *h, const short *len, int n) +{ + short offs[16]; + memset(h->count, 0, sizeof h->count); + for (int i = 0; i < n; i++) h->count[len[i]]++; + if (h->count[0] == n) return 0; + int left = 1; + for (int l = 1; l < 16; l++) { left <<= 1; left -= h->count[l]; if (left < 0) return left; } + offs[1] = 0; + for (int l = 1; l < 15; l++) offs[l + 1] = (short)(offs[l] + h->count[l]); + for (int i = 0; i < n; i++) if (len[i]) h->symbol[offs[len[i]]++] = (short)i; + return left; +} +static int inflate_codes(inf_t *s, const huff_t *lc, const huff_t *dc) +{ + static const short lbase[29] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258 }; + static const short lext[29] = { 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0 }; + static const short dbase[30] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577 }; + static const short dext[30] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + for (;;) { + int sym = hdecode(s, lc); + if (s->err || sym < 0) return 0; + if (sym < 256) { if (s->op >= s->outn) return 0; s->out[s->op++] = (unsigned char)sym; } + else if (sym == 256) return 1; + else { + sym -= 257; + if (sym >= 29) return 0; + int len = lbase[sym] + ibits(s, lext[sym]); + int ds = hdecode(s, dc); + if (s->err || ds < 0 || ds >= 30) return 0; + size_t dist = (size_t)(dbase[ds] + ibits(s, dext[ds])); + if (dist > s->op || s->op + (size_t)len > s->outn) return 0; + for (int i = 0; i < len; i++, s->op++) s->out[s->op] = s->out[s->op - dist]; + } + } +} +static int inflate_mem(const unsigned char *in, size_t inn, unsigned char *out, size_t outn) +{ + inf_t s; memset(&s, 0, sizeof s); s.in = in; s.inn = inn; s.out = out; s.outn = outn; + static huff_t lc, dc; + int last; + do { + last = ibits(&s, 1); + int type = ibits(&s, 2); + if (s.err) return 0; + if (type == 0) { + s.bb = 0; s.bc = 0; + if (s.ip + 4 > s.inn) return 0; + unsigned len = s.in[s.ip] | (s.in[s.ip + 1] << 8); s.ip += 4; + if (s.ip + len > s.inn || s.op + len > s.outn) return 0; + memcpy(s.out + s.op, s.in + s.ip, len); s.ip += len; s.op += len; + } else if (type == 1) { + static int fixed_done; static huff_t flc, fdc; + if (!fixed_done) { + short l[288]; int i; + for (i = 0; i < 144; i++) l[i] = 8; + for (; i < 256; i++) l[i] = 9; + for (; i < 280; i++) l[i] = 7; + for (; i < 288; i++) l[i] = 8; + hbuild(&flc, l, 288); + for (i = 0; i < 30; i++) l[i] = 5; + hbuild(&fdc, l, 30); + fixed_done = 1; + } + if (!inflate_codes(&s, &flc, &fdc)) return 0; + } else if (type == 2) { + static const unsigned char ord[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + short l[320]; int nlen = ibits(&s, 5) + 257, ndist = ibits(&s, 5) + 1, ncode = ibits(&s, 4) + 4, i; + if (s.err || nlen > 286 || ndist > 30) return 0; + for (i = 0; i < ncode; i++) l[ord[i]] = (short)ibits(&s, 3); + for (; i < 19; i++) l[ord[i]] = 0; + if (hbuild(&lc, l, 19)) return 0; + for (i = 0; i < nlen + ndist;) { + int sym = hdecode(&s, &lc); + if (s.err || sym < 0) return 0; + if (sym < 16) l[i++] = (short)sym; + else { + int rep, v = 0; + if (sym == 16) { if (!i) return 0; v = l[i - 1]; rep = 3 + ibits(&s, 2); } + else if (sym == 17) rep = 3 + ibits(&s, 3); + else rep = 11 + ibits(&s, 7); + if (i + rep > nlen + ndist) return 0; + while (rep--) l[i++] = (short)v; + } + } + if (l[256] == 0) return 0; + if (hbuild(&lc, l, nlen) < 0 && nlen - lc.count[0] != 1) return 0; + if (hbuild(&dc, l + nlen, ndist) < 0 && ndist - dc.count[0] != 1) return 0; + if (!inflate_codes(&s, &lc, &dc)) return 0; + } else return 0; + } while (!last); + return s.op == s.outn; +} +static unsigned rd16(const unsigned char *p) { return p[0] | (p[1] << 8); } +static unsigned rd32(const unsigned char *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((unsigned)p[3] << 24); } +/* every entry of the zip into dir (which it creates); progress f0..f1 */ +static int unzip(const wchar_t *zip, const wchar_t *dir, double f0, double f1, const wchar_t *what) +{ + HANDLE f = CreateFileW(zip, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (f == INVALID_HANDLE_VALUE) return bld_fail(L"Could not open %ls.", zip); + LARGE_INTEGER sz; GetFileSizeEx(f, &sz); + size_t n = (size_t)sz.QuadPart; + unsigned char *z = malloc(n ? n : 1); + DWORD got = 0; size_t at = 0; + while (z && at < n && ReadFile(f, z + at, (DWORD)((n - at) > (1u << 30) ? (1u << 30) : (n - at)), &got, 0) && got) at += got; + CloseHandle(f); + if (!z || at != n) { free(z); return bld_fail(L"Could not read %ls.", zip); } + size_t e = n >= 22 ? n - 22 : 0, stop = n > 65557 ? n - 65557 : 0; + while (e > stop && rd32(z + e) != 0x06054b50) e--; + if (rd32(z + e) != 0x06054b50) { free(z); return bld_fail(L"%ls is not a zip file.", what); } + unsigned cnt = rd16(z + e + 10); size_t cd = rd32(z + e + 16); + mkdirs(dir); + int ok = 1; + for (unsigned i = 0; i < cnt && ok; i++) { + if (bld.cancel) { ok = 0; break; } + if (cd + 46 > n || rd32(z + cd) != 0x02014b50) { ok = bld_fail(L"%ls is damaged.", what); break; } + unsigned method = rd16(z + cd + 10), nl = rd16(z + cd + 28), xl = rd16(z + cd + 30), cl = rd16(z + cd + 32); + WORD dtime = (WORD)rd16(z + cd + 12), ddate = (WORD)rd16(z + cd + 14); /* (kept: a retry of the same kit reuses the compiled files) */ + size_t csize = rd32(z + cd + 20), usize = rd32(z + cd + 24), lo = rd32(z + cd + 42); + char name[512]; if (nl >= sizeof name) { ok = bld_fail(L"%ls has a name too long.", what); break; } + memcpy(name, z + cd + 46, nl); name[nl] = 0; + cd += 46 + nl + xl + cl; + if (strstr(name, "..") || name[0] == '/' || name[0] == '\\' || strchr(name, ':')) continue; /* (never outside dir) */ + wchar_t wn[512], p[MAX_PATH]; + MultiByteToWideChar(CP_UTF8, 0, name, -1, wn, 512); + for (wchar_t *c = wn; *c; c++) if (*c == L'/') *c = L'\\'; + path_join(p, dir, wn); + size_t wl = wcslen(p); + if (wl && p[wl - 1] == L'\\') { p[wl - 1] = 0; mkdirs(p); continue; } + { wchar_t d2[MAX_PATH]; wcscpy(d2, p); wchar_t *s2 = wcsrchr(d2, L'\\'); if (s2) { *s2 = 0; mkdirs(d2); } } + if (lo + 30 > n || rd32(z + lo) != 0x04034b50) { ok = bld_fail(L"%ls is damaged.", what); break; } + size_t data = lo + 30 + rd16(z + lo + 26) + rd16(z + lo + 28); + if (data + csize > n) { ok = bld_fail(L"%ls is damaged.", what); break; } + unsigned char *u = method == 0 ? z + data : malloc(usize ? usize : 1); + if (method == 8 && (!u || !inflate_mem(z + data, csize, u, usize))) { free(u); ok = bld_fail(L"%ls is damaged (%hs).", what, name); break; } + if (method != 0 && method != 8) { ok = bld_fail(L"%ls uses an unknown compression.", what); break; } + HANDLE o = CreateFileW(p, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); + if (o == INVALID_HANDLE_VALUE) { if (method == 8) free(u); ok = bld_fail(L"Could not write %ls.", p); break; } + size_t w0 = 0; DWORD w; + while (w0 < usize && WriteFile(o, u + w0, (DWORD)((usize - w0) > (1u << 30) ? (1u << 30) : (usize - w0)), &w, 0) && w) w0 += w; + { FILETIME lt, ft; if (DosDateTimeToFileTime(ddate, dtime, <) && LocalFileTimeToFileTime(<, &ft)) SetFileTime(o, 0, 0, &ft); } + CloseHandle(o); + if (method == 8) free(u); + if (w0 != usize) { ok = bld_fail(L"Could not write %ls (disk full?).", p); break; } + if (i % 16 == 0) bld_status(f0 + (f1 - f0) * (double)(i + 1) / cnt, L"Unpacking %ls (%u of %u files)", what, i + 1, cnt); + } + free(z); + return ok && !bld.cancel; +} + +/* ---- the release's description: a flat JSON object ---- */ +static int json_get(const char *js, const char *key, char *out, size_t n) +{ + char k[96]; snprintf(k, sizeof k, "\"%s\"", key); + const char *p = strstr(js, k); + if (!p) return 0; + p += strlen(k); + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ':') p++; + size_t i = 0; + if (*p == '"') { p++; while (*p && *p != '"' && i + 1 < n) out[i++] = *p++; } + else while (*p && *p != ',' && *p != '}' && *p != ' ' && *p != '\r' && *p != '\n' && i + 1 < n) out[i++] = *p++; + out[i] = 0; + return 1; +} + +/* ---- running build.py ---- */ +static void build_line(const char *l, double f0, double f1) +{ + char ph[32]; int a, b; + if (sscanf(l, "[progress] %31s %d %d", ph, &a, &b) != 3 || b <= 0) return; + double x = (double)a / b, lo, hi; /* where each phase sits between f0 and f1 */ + const wchar_t *what; + if (!strcmp(ph, "prepare")) { lo = 0.00; hi = 0.08; what = L"Translating the game's executable"; } + else if (!strcmp(ph, "levels")) { lo = 0.08; hi = 0.30; what = L"Translating the levels"; } + else if (!strcmp(ph, "compile")) { lo = 0.30; hi = 0.97; what = L"Compiling"; } + else if (!strcmp(ph, "link")) { lo = 0.97; hi = 0.99; what = L"Linking"; } + else { lo = 1; hi = 1; what = L"Finishing"; } + double f = f0 + (f1 - f0) * (lo + (hi - lo) * x); + if (!strcmp(ph, "levels")) bld_status(f, L"%ls (%d of %d)", what, a + 1, b); + else if (!strcmp(ph, "compile")) bld_status(f, L"%ls (%d of %d files)", what, a, b); + else bld_status(f, L"%ls", what); +} +static int run_build(const wchar_t *gdir, const wchar_t *iso, const wchar_t *levels, double f0, double f1) +{ + wchar_t py[MAX_PATH], zig[MAX_PATH], tc[MAX_PATH], cmd[4096], logp[MAX_PATH]; + path_join(tc, base_dir, L"toolchain"); + swprintf(py, MAX_PATH, L"%ls\\python\\python.exe", tc); + swprintf(zig, MAX_PATH, L"%ls\\zig", tc); + /* jobs: the cores, but no more than the memory allows (a big generated file can take 1.5 GB to compile) */ + SYSTEM_INFO si; GetSystemInfo(&si); + MEMORYSTATUSEX ms; ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms); + int j = (int)si.dwNumberOfProcessors, jm = (int)(ms.ullTotalPhys / (1536ull << 20)); + if (j > jm) j = jm; + if (j < 1) j = 1; + swprintf(cmd, 4096, L"\"%ls\" -u build.py --platform windows --levels %ls --iso \"%ls\" --repo decomp -j %d", py, levels, iso, j); + static wchar_t path[8192]; static int have_path; /* (the PATH this program started with) */ + if (!have_path) { DWORD k = GetEnvironmentVariableW(L"PATH", path, 8192); if (k >= 8192) path[0] = 0; have_path = 1; } + { /* the toolchain first on PATH; Python in UTF-8 mode (the sources are UTF-8) */ + wchar_t np[9000]; + swprintf(np, 9000, L"%ls;%ls\\python;%ls", zig, tc, path); + SetEnvironmentVariableW(L"PATH", np); + SetEnvironmentVariableW(L"PYTHONUTF8", L"1"); + SetEnvironmentVariableW(L"PYTHONDONTWRITEBYTECODE", L"1"); + SetEnvironmentVariableW(L"PYTHONHOME", 0); SetEnvironmentVariableW(L"PYTHONPATH", 0); + swprintf(np, 9000, L"%ls\\zig-cache", gdir); + SetEnvironmentVariableW(L"ZIG_GLOBAL_CACHE_DIR", np); + SetEnvironmentVariableW(L"ZIG_LOCAL_CACHE_DIR", np); + } + path_join(logp, gdir, L"build_log.txt"); + HANDLE lf = CreateFileW(logp, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); + SECURITY_ATTRIBUTES sa = { sizeof sa, 0, TRUE }; + HANDLE rd, wr; + if (!CreatePipe(&rd, &wr, &sa, 0)) { if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); return bld_fail(L"Could not start the build (pipe)."); } + SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); + STARTUPINFOW st; PROCESS_INFORMATION pi; + memset(&st, 0, sizeof st); st.cb = sizeof st; + st.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; st.wShowWindow = SW_HIDE; + st.hStdOutput = wr; st.hStdError = wr; st.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + bld.job = CreateJobObjectW(0, 0); + if (bld.job) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION li; memset(&li, 0, sizeof li); li.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(bld.job, JobObjectExtendedLimitInformation, &li, sizeof li); } + if (!CreateProcessW(py, cmd, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, gdir, &st, &pi)) { + CloseHandle(rd); CloseHandle(wr); if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); + return bld_fail(L"Could not start the build tools (error %lu).", GetLastError()); + } + if (bld.job) AssignProcessToJobObject(bld.job, pi.hProcess); + ResumeThread(pi.hThread); CloseHandle(pi.hThread); + CloseHandle(wr); + static char tail[16][300]; int nt = 0; + char line[4096]; size_t ln = 0; char b[4096]; DWORD n; + while (ReadFile(rd, b, sizeof b, &n, 0) && n) { + DWORD w; if (lf != INVALID_HANDLE_VALUE) WriteFile(lf, b, n, &w, 0); + for (DWORD i = 0; i < n; i++) { + if (b[i] == '\n' || ln + 1 >= sizeof line) { + line[ln] = 0; if (ln && line[ln - 1] == '\r') line[ln - 1] = 0; + if (!strncmp(line, "[progress]", 10)) build_line(line, f0, f1); + else if (line[0]) { snprintf(tail[nt % 16], 300, "%s", line); nt++; } + ln = 0; + } else line[ln++] = b[i]; + } + if (bld.cancel) TerminateJobObject(bld.job, 1); + } + CloseHandle(rd); + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD code = 1; GetExitCodeProcess(pi.hProcess, &code); + CloseHandle(pi.hProcess); + if (bld.job) { CloseHandle(bld.job); bld.job = 0; } + if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); + if (bld.cancel) return 0; + if (code) { + const char *last = nt ? tail[(nt - 1) % 16] : "no output"; + return bld_fail(L"The build stopped (code %lu): %hs", code, last); + } + return 1; +} + +/* ---- the Build button's thread ---- */ +static DWORD WINAPI build_thread(LPVOID unused) +{ + (void)unused; + disc_t *d = &discs[bld.disc]; + const game_t *g = &games[d->game]; + wchar_t rel[1024], url[1200], dl[MAX_PATH], tcdir[MAX_PATH], gdir[MAX_PATH], zp[MAX_PATH], p[MAX_PATH]; + char *js = 0; size_t jn = 0; + GetPrivateProfileStringW(L"build", L"release_url", RELEASE_URL, rel, 1024, ini_path); + int local = _wcsnicmp(rel, L"http", 4) != 0; + const wchar_t *sep = local ? L"\\" : L"/"; + size_t rl = wcslen(rel); if (rl && (rel[rl - 1] == L'/' || rel[rl - 1] == L'\\')) rel[--rl] = 0; + path_join(dl, base_dir, L"downloads"); mkdirs(dl); + path_join(tcdir, base_dir, L"toolchain"); + swprintf(gdir, MAX_PATH, L"%ls\\games\\%ls", base_dir, g->kit); + + /* 1. what to fetch */ + bld_status(0.0, L"Checking for the latest build files"); + swprintf(url, 1200, L"%ls%ls%ls%ls%ls.json", rel, sep, g->kit, sep, g->kit); + if (!fetch(url, 0, &js, &jn, 0.0, 0.01, L"the build description")) goto out; + char version[64] = "", tcv[64] = "", tcf[128] = "", tcs[80] = "", kitf[128] = "", kits[80] = "", lv[256] = ""; + json_get(js, "version", version, sizeof version); + json_get(js, "toolchain_version", tcv, sizeof tcv); json_get(js, "toolchain_file", tcf, sizeof tcf); json_get(js, "toolchain_sha256", tcs, sizeof tcs); + json_get(js, "kit_file", kitf, sizeof kitf); json_get(js, "kit_sha256", kits, sizeof kits); + json_get(js, "levels", lv, sizeof lv); + if (!version[0] || !tcf[0] || !kitf[0]) { bld_fail(L"The build description from the release is not valid."); goto out; } + + /* 2. the toolchain, when it is missing or another version */ + { + wchar_t have[64] = L"", want[64], vf[MAX_PATH]; + MultiByteToWideChar(CP_UTF8, 0, tcv, -1, want, 64); + path_join(vf, tcdir, L"version.txt"); + { FILE *f = _wfopen(vf, L"r"); if (f) { char b[64] = ""; if (fgets(b, sizeof b, f)) { b[strcspn(b, "\r\n")] = 0; MultiByteToWideChar(CP_UTF8, 0, b, -1, have, 64); } fclose(f); } } + swprintf(p, MAX_PATH, L"%ls\\python\\python.exe", tcdir); + if (wcscmp(have, want) || GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { + wchar_t wf[128]; MultiByteToWideChar(CP_UTF8, 0, tcf, -1, wf, 128); + swprintf(url, 1200, L"%ls%ls%ls%ls%ls", rel, sep, g->kit, sep, wf); + path_join(zp, dl, wf); + if (!fetch(url, zp, 0, 0, 0.01, 0.20, L"Downloading the build tools")) goto out; + bld_status(0.20, L"Checking the build tools"); + char h[65]; if (tcs[0] && (!sha256_file(zp, h) || _stricmp(h, tcs))) { DeleteFileW(zp); bld_fail(L"The downloaded build tools are damaged (checksum). Try again."); goto out; } + rm_tree(tcdir); + if (!unzip(zp, tcdir, 0.20, 0.26, L"the build tools")) goto out; + FILE *f = _wfopen(vf, L"w"); if (f) { fputs(tcv, f); fclose(f); } + DeleteFileW(zp); + } + } + /* 3. the game's build kit (a fresh copy; the game's own folder, build\, stays: saves live there) */ + { + wchar_t wf[128]; MultiByteToWideChar(CP_UTF8, 0, kitf, -1, wf, 128); + swprintf(url, 1200, L"%ls%ls%ls%ls%ls", rel, sep, g->kit, sep, wf); + path_join(zp, dl, wf); + if (!fetch(url, zp, 0, 0, 0.26, 0.30, L"Downloading the game's build kit")) goto out; + char h[65]; if (kits[0] && (!sha256_file(zp, h) || _stricmp(h, kits))) { DeleteFileW(zp); bld_fail(L"The downloaded build kit is damaged (checksum). Try again."); goto out; } + mkdirs(gdir); + { /* another kit version than the files compiled last time: none of them can be reused */ + wchar_t kv[64] = L"", wv[64]; + GetPrivateProfileStringW(g->kit, L"kit_version", L"", kv, 64, ini_path); + MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64); + if (kv[0] && wcscmp(kv, wv)) { /* (none recorded: a first build, or one from before this was kept) */ + static const wchar_t *old[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj", L"zig-cache" }; + for (unsigned i = 0; i < sizeof old / sizeof old[0]; i++) { path_join(p, gdir, old[i]); rm_tree(p); } + } + WritePrivateProfileStringW(g->kit, L"kit_version", wv, ini_path); + } + { /* everything but build\ */ + wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; swprintf(pat, MAX_PATH, L"%ls\\*", gdir); + HANDLE hf = FindFirstFileW(pat, &fd); + if (hf != INVALID_HANDLE_VALUE) { + do { if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..") || !_wcsicmp(fd.cFileName, L"build")) continue; + path_join(p, gdir, fd.cFileName); rm_tree(p); } while (FindNextFileW(hf, &fd)); + FindClose(hf); + } + } + if (!unzip(zp, gdir, 0.30, 0.32, L"the build kit")) goto out; + DeleteFileW(zp); + } + /* 4. build */ + { + wchar_t levels[256]; + GetPrivateProfileStringW(L"build", L"levels", L"", levels, 256, ini_path); + if (!levels[0]) MultiByteToWideChar(CP_UTF8, 0, lv[0] ? lv : "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", -1, levels, 256); + bld_status(0.32, L"Starting the build"); + if (!run_build(gdir, d->path, levels, 0.32, 0.99)) goto out; + } + /* 5. keep what the game needs: the intermediate files (about 1.3 GB) and the bytes read from the disc go */ + bld_status(0.99, L"Cleaning up"); + { + static const wchar_t *junk[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj", L"build\\levels", L"build\\disc", + L"build\\windows\\rac1_game.pdb", L"build\\windows\\rac1_wrapper.pdb", L"build\\windows\\asm_stubs.c.lib", L"zig-cache" }; + for (unsigned i = 0; i < sizeof junk / sizeof junk[0]; i++) { path_join(p, gdir, junk[i]); rm_tree(p); } + } + { wchar_t wv[64]; MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64); + WritePrivateProfileStringW(g->kit, L"built_version", wv, ini_path); } + bld_status(1.0, L"Built"); +out: + free(js); + if (bld.cancel && !bld.failed) bld_fail(L"Build cancelled."); + bld.active = 0; + PostMessageW(hwnd, WM_BUILD_UPDATE, 1, 0); + return 0; +} +static void build_start(int i) +{ + if (bld.active || i < 0 || discs[i].game < 0 || !games[discs[i].game].kit) return; + bld.active = 1; bld.cancel = 0; bld.failed = 0; bld.disc = i; bld.frac = 0; bld.err[0] = 0; + wcscpy(bld.status, L"Starting"); + message[0] = 0; + CloseHandle(CreateThread(0, 0, build_thread, 0, 0, 0)); +} diff --git a/launcher/solana_img.h b/launcher/solana_img.h new file mode 100644 index 0000000..78b9e0f --- /dev/null +++ b/launcher/solana_img.h @@ -0,0 +1,128 @@ +/* solana_img.h: the launcher's art (included once by solana.c). + * + * The PNGs in assets/png are built into the exe as RCDATA resources (solana.rc). + * They are decoded with WIC (part of Windows), scaled with its high-quality + * (Fant) scaler to the size they are drawn at for the current DPI, optionally + * tinted (the white icons), and drawn with AlphaBlend. Scaled copies are kept + * until the DPI changes. */ +#define COBJMACROS +#include + +/* resource ids (solana.rc) */ +enum { + IMG_BANNER = 100, IMG_WORDMARK, IMG_WORDMARK_TEXT, IMG_EMPTY, IMG_BGTILE, + IMG_I_ADD = 120, IMG_I_BUILD, IMG_I_CANCEL, IMG_I_FOLDER, IMG_I_LOG, IMG_I_PLAY, IMG_I_REBUILD, IMG_I_REMOVE, IMG_I_SCAN, IMG_I_SETTINGS, + IMG_PS_CROSS = 140, IMG_PS_CIRCLE, IMG_PS_SQUARE, IMG_PS_TRIANGLE, IMG_PS_L1, IMG_PS_R1, IMG_PS_L2, IMG_PS_R2, IMG_PS_START, IMG_PS_SELECT, + IMG_XB_A = 160, IMG_XB_B, IMG_XB_X, IMG_XB_Y, IMG_XB_LB, IMG_XB_RB, IMG_XB_LT, IMG_XB_RT, IMG_XB_MENU, IMG_XB_VIEW, +}; + +static IWICImagingFactory *wic; +typedef struct { int id, w, h, tinted; COLORREF tint; HBITMAP bmp; } img_t; +#define NIMG 160 +static img_t imgs[NIMG]; +static int nimgs; + +static HBRUSH tile_br; static int tile_sz; +static void img_flush(void) /* (the DPI changed) */ +{ + if (tile_br) { DeleteObject(tile_br); tile_br = 0; } + for (int i = 0; i < nimgs; i++) if (imgs[i].bmp) DeleteObject(imgs[i].bmp); + nimgs = 0; +} +/* the resource's natural size */ +static IWICBitmapFrameDecode *img_frame(int id) +{ + if (!wic && FAILED(CoCreateInstance(&CLSID_WICImagingFactory, 0, CLSCTX_INPROC_SERVER, &IID_IWICImagingFactory, (void **)&wic))) return 0; + HRSRC r = FindResourceW(0, MAKEINTRESOURCEW(id), (LPCWSTR)RT_RCDATA); + if (!r) return 0; + HGLOBAL g = LoadResource(0, r); + void *p = g ? LockResource(g) : 0; + DWORD n = SizeofResource(0, r); + if (!p || !n) return 0; + IWICStream *st = 0; IWICBitmapDecoder *dec = 0; IWICBitmapFrameDecode *fr = 0; + if (SUCCEEDED(IWICImagingFactory_CreateStream(wic, &st)) && SUCCEEDED(IWICStream_InitializeFromMemory(st, (BYTE *)p, n)) && + SUCCEEDED(IWICImagingFactory_CreateDecoderFromStream(wic, (IStream *)st, 0, WICDecodeMetadataCacheOnDemand, &dec))) + IWICBitmapDecoder_GetFrame(dec, 0, &fr); + if (dec) IWICBitmapDecoder_Release(dec); + if (st) IWICStream_Release(st); + return fr; +} +static int img_size(int id, int *w, int *h) +{ + static int cid[64], cw[64], ch[64], nc; + for (int i = 0; i < nc; i++) if (cid[i] == id) { *w = cw[i]; *h = ch[i]; return 1; } + IWICBitmapFrameDecode *fr = img_frame(id); + if (!fr) return 0; + UINT a, b; IWICBitmapFrameDecode_GetSize(fr, &a, &b); IWICBitmapFrameDecode_Release(fr); + if (nc < 64) { cid[nc] = id; cw[nc] = (int)a; ch[nc] = (int)b; nc++; } + *w = (int)a; *h = (int)b; + return 1; +} +/* a premultiplied 32-bit DIB of the image at w x h (tinted: its alpha in that colour) */ +static HBITMAP img_get(int id, int w, int h, int tinted, COLORREF tint) +{ + if (w < 1 || h < 1 || w > 8192 || h > 8192) return 0; + for (int i = 0; i < nimgs; i++) + if (imgs[i].id == id && imgs[i].w == w && imgs[i].h == h && imgs[i].tinted == tinted && (!tinted || imgs[i].tint == tint)) return imgs[i].bmp; + IWICBitmapFrameDecode *fr = img_frame(id); + if (!fr) return 0; + IWICBitmapScaler *sc = 0; IWICFormatConverter *cv = 0; HBITMAP bmp = 0; + if (SUCCEEDED(IWICImagingFactory_CreateBitmapScaler(wic, &sc)) && + SUCCEEDED(IWICBitmapScaler_Initialize(sc, (IWICBitmapSource *)fr, (UINT)w, (UINT)h, WICBitmapInterpolationModeFant)) && + SUCCEEDED(IWICImagingFactory_CreateFormatConverter(wic, &cv)) && + SUCCEEDED(IWICFormatConverter_Initialize(cv, (IWICBitmapSource *)sc, &GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, 0, 0, WICBitmapPaletteTypeCustom))) { + BITMAPINFO bi; memset(&bi, 0, sizeof bi); + bi.bmiHeader.biSize = sizeof bi.bmiHeader; bi.bmiHeader.biWidth = w; bi.bmiHeader.biHeight = -h; + bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB; + void *bits = 0; + bmp = CreateDIBSection(0, &bi, DIB_RGB_COLORS, &bits, 0, 0); + if (bmp && SUCCEEDED(IWICFormatConverter_CopyPixels(cv, 0, (UINT)w * 4, (UINT)(w * h * 4), (BYTE *)bits))) { + if (tinted) { + unsigned *px = bits, r = GetRValue(tint), g = GetGValue(tint), b = GetBValue(tint); + for (int i = 0; i < w * h; i++) { unsigned a = px[i] >> 24; px[i] = (a << 24) | ((r * a / 255) << 16) | ((g * a / 255) << 8) | (b * a / 255); } + } + } else if (bmp) { DeleteObject(bmp); bmp = 0; } + } + if (cv) IWICFormatConverter_Release(cv); + if (sc) IWICBitmapScaler_Release(sc); + IWICBitmapFrameDecode_Release(fr); + if (!bmp) return 0; + if (nimgs == NIMG) img_flush(); + imgs[nimgs].id = id; imgs[nimgs].w = w; imgs[nimgs].h = h; imgs[nimgs].tinted = tinted; imgs[nimgs].tint = tint; imgs[nimgs].bmp = bmp; nimgs++; + return bmp; +} +/* part (sx, sy, sw, sh) of the image scaled to w x h, at (x, y) */ +static int img_blit(HDC dc, int id, int w, int h, int sx, int sy, int sw, int sh, int x, int y, int tinted, COLORREF tint, int alpha) +{ + HBITMAP b = img_get(id, w, h, tinted, tint); + if (!b) return 0; + HDC m = CreateCompatibleDC(dc); + HGDIOBJ old = SelectObject(m, b); + BLENDFUNCTION bf = { AC_SRC_OVER, 0, (BYTE)alpha, AC_SRC_ALPHA }; + AlphaBlend(dc, x, y, sw, sh, m, sx, sy, sw, sh, bf); + SelectObject(m, old); DeleteDC(m); + return 1; +} +static int img_draw(HDC dc, int id, int x, int y, int w, int h) { return img_blit(dc, id, w, h, 0, 0, w, h, x, y, 0, 0, 255); } +static int img_icon(HDC dc, int id, int x, int y, int sz, COLORREF c) { return img_blit(dc, id, sz, sz, 0, 0, sz, sz, x, y, 1, c, 255); } +/* the image filling r (cropped, never stretched): anchored right (ax = 1) or centred (0.5), centred vertically */ +static int img_cover(HDC dc, int id, RECT r, double ax) +{ + int iw, ih, w = r.right - r.left, h = r.bottom - r.top; + if (w <= 0 || h <= 0 || !img_size(id, &iw, &ih)) return 0; + double s = (double)w / iw; if ((double)h / ih > s) s = (double)h / ih; + int sw = (int)(iw * s + 0.999), sh = (int)(ih * s + 0.999); + { /* the banner is resized with the window: keep only the latest size of it */ + for (int i = 0; i < nimgs; i++) if (imgs[i].id == id && !imgs[i].tinted && (imgs[i].w != sw || imgs[i].h != sh)) { DeleteObject(imgs[i].bmp); imgs[i] = imgs[--nimgs]; i--; } + } + return img_blit(dc, id, sw, sh, (int)((sw - w) * ax), (sh - h) / 2, w, h, r.left, r.top, 0, 0, 255); +} +/* a pattern brush of the tile at the current DPI (0 if there is none) */ +static HBRUSH img_tile_brush(int id, int size) +{ + if (tile_br && tile_sz == size) return tile_br; + if (tile_br) { DeleteObject(tile_br); tile_br = 0; } + HBITMAP b = img_get(id, size, size, 0, 0); + if (b) { tile_br = CreatePatternBrush(b); tile_sz = size; } + return tile_br; +} diff --git a/release/make_release.py b/release/make_release.py new file mode 100644 index 0000000..333fb07 --- /dev/null +++ b/release/make_release.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""make_release.py: makes the files of a game's GitHub release (the tag is the game, e.g. "rac1"). + +usage: python release/make_release.py rac1 --decomp PATH_TO_RAC1_DECOMP [--version V] + [--toolchain-zip toolchain-win64.zip] + +Writes dist/rac1/: + rac1.json what the launcher's Build button fetches (version, files, SHA-256s, levels) + rac1-build.zip games/rac1 (build.py, tools/, host/, launcher/, docs/) plus the decomp's + source as decomp/ (everything but baserom/ and .git: no game files) + toolchain-win64.zip Python, rabbitizer and zig for Windows x64 (release/toolchain.json); reused + from --toolchain-zip when given, else fetched (PyPI and GitHub) and assembled + +Upload the three files to the release named after the game (gh release create rac1 dist/rac1/*). +Players never download anything from the game itself: the build reads their own disc image.""" +import argparse, datetime, glob, hashlib, io, json, os, shutil, subprocess, sys, tarfile, tempfile, zipfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +ap = argparse.ArgumentParser() +ap.add_argument("game") +ap.add_argument("--decomp", required=True, help="the game's decomp repo (only its source is packed; baserom/ is left out)") +ap.add_argument("--version", default=datetime.date.today().strftime("%Y.%m.%d")) +ap.add_argument("--toolchain-zip", default=None) +ap.add_argument("--levels", default=",".join(str(i) for i in range(19))) +a = ap.parse_args() + +game = a.game +src = os.path.join(ROOT, "games", game) +if not os.path.isdir(src): sys.exit("no games/%s" % game) +out = os.path.join(ROOT, "dist", game) +os.makedirs(out, exist_ok=True) + +SKIP_DIRS = {"__pycache__", ".git", "build", "dev", "decomp", "zig-cache"} +DECOMP_SKIP = {"baserom", ".git", ".github"} + + +def add_tree(z, top, arc, skip): + for root, dirs, files in os.walk(top): + dirs[:] = sorted(d for d in dirs if d not in skip) + for f in sorted(files): + if f.endswith((".pyc", ".o", ".obj")) or f == "build_log.txt": continue + p = os.path.join(root, f) + # one fixed, old timestamp for every file: files the player compiled before are + # newer, so a retry of the same kit reuses them (a new kit version starts clean) + zi = zipfile.ZipInfo(os.path.join(arc, os.path.relpath(p, top)).replace(os.sep, "/"), date_time=(2020, 1, 1, 0, 0, 0)) + zi.compress_type = zipfile.ZIP_DEFLATED + zi.external_attr = 0o644 << 16 + with open(p, "rb") as fh: z.writestr(zi, fh.read(), compresslevel=9) + + +def sha256(p): + h = hashlib.sha256() + with open(p, "rb") as f: + for b in iter(lambda: f.read(1 << 20), b""): h.update(b) + return h.hexdigest() + + +# the build kit +kit = os.path.join(out, "%s-build.zip" % game) +with zipfile.ZipFile(kit, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: + add_tree(z, src, "", SKIP_DIRS) + add_tree(z, a.decomp, "decomp", DECOMP_SKIP) + for f in ("THIRD_PARTY_NOTICES.md",): + if os.path.exists(os.path.join(ROOT, f)): z.write(os.path.join(ROOT, f), f) + for n in z.namelist(): # (nothing from the game: no executable, no overlays) + if n.startswith("decomp/baserom/") or n.endswith(("SCES_509.16", ".iso")): sys.exit("refusing to pack %s" % n) +print("kit:", kit, os.path.getsize(kit) // 1024, "KB") + +# the toolchain +tcj = json.load(open(os.path.join(HERE, "toolchain.json"))) +tcz = os.path.join(out, "toolchain-win64.zip") +if a.toolchain_zip: + if os.path.abspath(a.toolchain_zip) != os.path.abspath(tcz): shutil.copyfile(a.toolchain_zip, tcz) +else: + import urllib.request + with tempfile.TemporaryDirectory() as t: + print("fetching", tcj["python"]) + data = urllib.request.urlopen(tcj["python"]).read() + tarfile.open(fileobj=io.BytesIO(data)).extractall(t) + py = os.path.join(t, "python") + for pat in ("*.pdb", "**/*.pdb"): + for p in glob.glob(os.path.join(py, pat), recursive=True): os.remove(p) + for d in ("tcl", "Lib/test", "Lib/idlelib", "Lib/tkinter", "Lib/turtledemo", "Lib/ensurepip", "include", "libs", "Scripts"): + shutil.rmtree(os.path.join(py, d), ignore_errors=True) + w = os.path.join(t, "wheels") + subprocess.run([sys.executable, "-m", "pip", "download", "--no-deps", "--platform", "win_amd64", "--only-binary=:all:", "-d", w, + tcj["ziglang_wheel"], tcj["rabbitizer_wheel"]], check=True) + for whl in glob.glob(os.path.join(w, "rabbitizer-*.whl")): zipfile.ZipFile(whl).extractall(os.path.join(py, "Lib", "site-packages")) + for whl in glob.glob(os.path.join(w, "ziglang-*.whl")): zipfile.ZipFile(whl).extractall(os.path.join(t, "zw")) + zg = os.path.join(t, "zig") + shutil.move(os.path.join(t, "zw", "ziglang"), zg) + for f in ("__init__.py", "__main__.py"): + if os.path.exists(os.path.join(zg, f)): os.remove(os.path.join(zg, f)) + # the C library sources and headers of other systems are not needed for x86-windows-gnu + lc = os.path.join(zg, "lib", "libc") + for d in os.listdir(lc): + if d in ("glibc", "musl", "darwin", "wasi", "freebsd", "netbsd", "openbsd"): shutil.rmtree(os.path.join(lc, d)) + inc = os.path.join(lc, "include") + for d in os.listdir(inc): + if "windows" not in d: shutil.rmtree(os.path.join(inc, d)) + for d in ("libcxx", "libcxxabi", "libunwind", "libtsan", "docs", "build-web", "init"): + shutil.rmtree(os.path.join(zg, "lib", d), ignore_errors=True) + with zipfile.ZipFile(tcz, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: + add_tree(z, py, "python", set()) + add_tree(z, zg, "zig", set()) +print("toolchain:", tcz, os.path.getsize(tcz) // (1 << 20), "MB") + +# what the launcher reads (a flat object: it is read without a JSON library) +man = { + "game": game, + "version": a.version, + "levels": a.levels, + "kit_file": os.path.basename(kit), "kit_size": os.path.getsize(kit), "kit_sha256": sha256(kit), + "toolchain_file": os.path.basename(tcz), "toolchain_size": os.path.getsize(tcz), "toolchain_sha256": sha256(tcz), + "toolchain_version": tcj["version"], +} +with open(os.path.join(out, "%s.json" % game), "w") as f: json.dump(man, f, indent=1) +print(json.dumps(man, indent=1)) diff --git a/release/rac1/rac1-build.zip b/release/rac1/rac1-build.zip new file mode 100644 index 0000000..6025623 Binary files /dev/null and b/release/rac1/rac1-build.zip differ diff --git a/release/rac1/rac1.json b/release/rac1/rac1.json new file mode 100644 index 0000000..7034d5b --- /dev/null +++ b/release/rac1/rac1.json @@ -0,0 +1,12 @@ +{ + "game": "rac1", + "version": "2026.10.13.2", + "levels": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", + "kit_file": "rac1-build.zip", + "kit_size": 5121567, + "kit_sha256": "abd1bf3d10e1ad8aa9f1c57eb06a609e7b3d0b90c4b9e0cbfead1e042d5934f9", + "toolchain_file": "toolchain-win64.zip", + "toolchain_size": 84006129, + "toolchain_sha256": "2d56894833ca4066faf5ac008d3fa06aa40e2bb1d1fcb8f5bd26f28a70382b63", + "toolchain_version": "py3.12.7-zig0.16.0-rabbitizer1.16.2" +} \ No newline at end of file diff --git a/release/rac1/toolchain-win64.zip b/release/rac1/toolchain-win64.zip new file mode 100644 index 0000000..72aee61 Binary files /dev/null and b/release/rac1/toolchain-win64.zip differ diff --git a/release/toolchain.json b/release/toolchain.json new file mode 100644 index 0000000..988b6ce --- /dev/null +++ b/release/toolchain.json @@ -0,0 +1,7 @@ +{ + "comment": "What toolchain-win64.zip is made of (release/make_release.py fetches these when it has no toolchain zip to reuse). All are redistributable: CPython (PSF licence), zig (MIT), rabbitizer (MIT).", + "version": "py3.12.7-zig0.16.0-rabbitizer1.16.2", + "python": "https://github.com/astral-sh/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-install_only.tar.gz", + "ziglang_wheel": "ziglang==0.16.0", + "rabbitizer_wheel": "rabbitizer==1.16.2" +}