mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-17 18:02:43 +02:00
On top of the shadps4-arm64 core imported in the previous commit: - PlayStation VR emulation: headset, tracker, reprojection, camera and social screen; the player's seat and view reset; head velocity for head-butts; field of view settings - Sound: a native Ngs2 engine (samplers, submixers, effects), the Audio3d spatializer, virtual surround for headset speakers, microphone pass-through; libSceJson2; libSceFiber on FEX - Fixes for CUSA12392 under emulation: real-time step, pace and resolution governor, larger render sizes on the PC, save loading at start - Renderer: geometry shaders fed with fans/polygons/loops, cube-array layers of light probes, sample-count mismatches on Adreno, timeline semaphore hang, FXAA in place of the MSAA resolve, sharpening, VR frame export - quest-host: the Quest app (OpenXR host that runs the glibc core as a child process, frames through AHardwareBuffers, DualSense mapping, hand-tracked controller placement, KGSL shim) - PC: in-process OpenXR host (Virtual Desktop or any OpenXR runtime), Touch controllers as the gamepad, launcher with a settings window; vendored OpenXR SDK 1.1.63 - Builds keep the build machine's folders out of the binaries (-ffile-prefix-map); machine settings (Python, headset serial, signing key) live in git-ignored tools/*.local files - tools: build, packaging and test scripts for Windows and the headset - README with installation, license (GPL-2.0-or-later), third-party notices and thanks No game files are part of the repository; .gitignore only lets listed paths in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.2 KiB
JavaScript
197 lines
8.2 KiB
JavaScript
// Minimal PS4 SELF/ELF inspector used to reverse-engineer how a game calls HLE functions.
|
|
//
|
|
// node ps4elf.mjs unwrap <eboot.bin> <out.elf> write a plain ELF (fSELF container removed)
|
|
// node ps4elf.mjs imports <eboot.bin> [regex] imported symbols with their PLT stub address
|
|
// node ps4elf.mjs callers <eboot.bin> <regex> call sites (E8/E9 rel32) of matching imports
|
|
// node ps4elf.mjs bytes <eboot.bin> <vaddr> <len> hex dump of mapped bytes at a virtual address
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const SELF_MAGIC = 0x1d3d154f;
|
|
const PT_LOAD = 1, PT_DYNAMIC = 2, PT_SCE_DYNLIBDATA = 0x61000000, PT_SCE_RELRO = 0x61000010;
|
|
const DT = {
|
|
SCE_JMPREL: 0x61000029, SCE_PLTRELSZ: 0x6100002d, SCE_RELA: 0x6100002f, SCE_RELASZ: 0x61000031,
|
|
SCE_STRTAB: 0x61000035, SCE_STRSZ: 0x61000037, SCE_SYMTAB: 0x61000039, SCE_SYMTABSZ: 0x6100003f,
|
|
};
|
|
|
|
export function loadElf(path) {
|
|
const file = readFileSync(path);
|
|
let elfOff = 0;
|
|
let selfSegments = null;
|
|
if (file.readUInt32LE(0) === SELF_MAGIC) {
|
|
const segCount = file.readUInt16LE(0x18);
|
|
selfSegments = [];
|
|
for (let i = 0; i < segCount; i++) {
|
|
const o = 0x20 + i * 0x20;
|
|
selfSegments.push({
|
|
flags: file.readBigUInt64LE(o),
|
|
offset: Number(file.readBigUInt64LE(o + 8)),
|
|
fileSize: Number(file.readBigUInt64LE(o + 16)),
|
|
memSize: Number(file.readBigUInt64LE(o + 24)),
|
|
});
|
|
}
|
|
elfOff = 0x20 + segCount * 0x20;
|
|
}
|
|
const eh = file.subarray(elfOff);
|
|
if (eh.readUInt32BE(0) !== 0x7f454c46) throw new Error("not an ELF");
|
|
const phoff = Number(eh.readBigUInt64LE(0x20));
|
|
const phentsize = eh.readUInt16LE(0x36);
|
|
const phnum = eh.readUInt16LE(0x38);
|
|
const phdrs = [];
|
|
for (let i = 0; i < phnum; i++) {
|
|
const o = phoff + i * phentsize;
|
|
phdrs.push({
|
|
index: i,
|
|
type: eh.readUInt32LE(o),
|
|
flags: eh.readUInt32LE(o + 4),
|
|
offset: Number(eh.readBigUInt64LE(o + 8)),
|
|
vaddr: Number(eh.readBigUInt64LE(o + 16)),
|
|
filesz: Number(eh.readBigUInt64LE(o + 32)),
|
|
memsz: Number(eh.readBigUInt64LE(o + 40)),
|
|
});
|
|
}
|
|
// Where does each program header's data live in the container?
|
|
const dataOf = (ph) => {
|
|
if (!selfSegments) return file.subarray(ph.offset, ph.offset + ph.filesz);
|
|
for (const seg of selfSegments) {
|
|
const blocked = (seg.flags & 0x800n) !== 0n;
|
|
const id = Number((seg.flags >> 20n) & 0xfffn);
|
|
if (blocked && id === ph.index) return file.subarray(seg.offset, seg.offset + ph.filesz);
|
|
}
|
|
return null;
|
|
};
|
|
for (const ph of phdrs) ph.data = ph.filesz ? dataOf(ph) : null;
|
|
// Headers such as PT_DYNAMIC and PT_TLS have no container segment of their own; their bytes
|
|
// sit inside another header's file range.
|
|
for (const ph of phdrs) {
|
|
if (ph.data || !ph.filesz) continue;
|
|
const host = phdrs.find((p) => p.data && p !== ph && ph.offset >= p.offset &&
|
|
ph.offset + ph.filesz <= p.offset + p.filesz);
|
|
if (host) ph.data = host.data.subarray(ph.offset - host.offset, ph.offset - host.offset + ph.filesz);
|
|
}
|
|
const headerLen = phoff + phnum * phentsize;
|
|
return { file, elfOff, header: eh.subarray(0, headerLen), phdrs, isSelf: !!selfSegments };
|
|
}
|
|
|
|
export function unwrap(elf) {
|
|
let size = elf.header.length;
|
|
for (const ph of elf.phdrs) if (ph.data) size = Math.max(size, ph.offset + ph.filesz);
|
|
const out = Buffer.alloc(size);
|
|
elf.header.copy(out, 0);
|
|
for (const ph of elf.phdrs) if (ph.data) ph.data.copy(out, ph.offset);
|
|
// Present it as a normal shared object so stock binutils-style tools accept it.
|
|
out.writeUInt16LE(3, 0x10); // e_type = ET_DYN
|
|
out.writeUInt8(0, 7); // EI_OSABI = SYSV (was FreeBSD)
|
|
out.writeBigUInt64LE(0n, 0x28); // e_shoff
|
|
out.writeUInt16LE(0, 0x3c); // e_shnum
|
|
out.writeUInt16LE(0, 0x3e); // e_shstrndx
|
|
return out;
|
|
}
|
|
|
|
export function readVaddr(elf, vaddr, len) {
|
|
for (const ph of elf.phdrs) {
|
|
if ((ph.type === PT_LOAD || ph.type === PT_SCE_RELRO) && ph.data && vaddr >= ph.vaddr &&
|
|
vaddr + len <= ph.vaddr + ph.filesz) {
|
|
return ph.data.subarray(vaddr - ph.vaddr, vaddr - ph.vaddr + len);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function nidNames() {
|
|
const text = readFileSync(resolve(here, "../shadps4-arm64-main/scripts/aerolib.inl"), "latin1");
|
|
const map = new Map();
|
|
for (const m of text.matchAll(/STUB\("([^"]{11})",\s*([A-Za-z0-9_]+)\)/g)) map.set(m[1], m[2]);
|
|
return map;
|
|
}
|
|
|
|
export function imports(elf) {
|
|
const dynlib = elf.phdrs.find((p) => p.type === PT_SCE_DYNLIBDATA);
|
|
const dynamic = elf.phdrs.find((p) => p.type === PT_DYNAMIC);
|
|
if (!dynlib || !dynamic) throw new Error("no SCE dynamic data");
|
|
const tags = new Map();
|
|
for (let o = 0; o + 16 <= dynamic.data.length; o += 16) {
|
|
const tag = Number(dynamic.data.readBigUInt64LE(o));
|
|
if (tag === 0) break;
|
|
if (!tags.has(tag)) tags.set(tag, Number(dynamic.data.readBigUInt64LE(o + 8)));
|
|
}
|
|
const d = dynlib.data;
|
|
const strtab = tags.get(DT.SCE_STRTAB), symtab = tags.get(DT.SCE_SYMTAB);
|
|
const names = nidNames();
|
|
const symName = (index) => {
|
|
const o = symtab + index * 24;
|
|
const nameOff = strtab + d.readUInt32LE(o);
|
|
let end = nameOff;
|
|
while (d[end] !== 0) end++;
|
|
return d.toString("latin1", nameOff, end);
|
|
};
|
|
const result = [];
|
|
const readRela = (off, size, kind) => {
|
|
for (let o = off; o + 24 <= off + size; o += 24) {
|
|
const slot = Number(d.readBigUInt64LE(o));
|
|
const info = d.readBigUInt64LE(o + 8);
|
|
const type = Number(info & 0xffffffffn);
|
|
const sym = Number(info >> 32n);
|
|
if (type !== 7 && type !== 6) continue; // JUMP_SLOT / GLOB_DAT
|
|
const raw = symName(sym);
|
|
const nid = raw.split("#")[0];
|
|
result.push({ slot, nid, raw, name: names.get(nid) ?? raw, kind: type === 7 ? "func" : "data" });
|
|
}
|
|
};
|
|
if (tags.has(DT.SCE_JMPREL)) readRela(tags.get(DT.SCE_JMPREL), tags.get(DT.SCE_PLTRELSZ), "plt");
|
|
if (tags.has(DT.SCE_RELA)) readRela(tags.get(DT.SCE_RELA), tags.get(DT.SCE_RELASZ), "rela");
|
|
|
|
// Locate each import's PLT stub: `jmp qword ptr [rip + disp32]` pointing at its GOT slot.
|
|
const text = elf.phdrs.find((p) => p.type === PT_LOAD && (p.flags & 1));
|
|
const bySlot = new Map(result.map((r) => [r.slot, r]));
|
|
const t = text.data;
|
|
for (let i = 0; i + 6 <= t.length; i++) {
|
|
if (t[i] !== 0xff || t[i + 1] !== 0x25) continue;
|
|
const target = text.vaddr + i + 6 + t.readInt32LE(i + 2);
|
|
const imp = bySlot.get(target);
|
|
if (imp && imp.stub === undefined) imp.stub = text.vaddr + i;
|
|
}
|
|
return { list: result, text };
|
|
}
|
|
|
|
export function callers(elf, regex) {
|
|
const { list, text } = imports(elf);
|
|
const wanted = new Map();
|
|
for (const imp of list) if (imp.stub !== undefined && regex.test(imp.name)) wanted.set(imp.stub, imp);
|
|
const t = text.data;
|
|
const hits = [];
|
|
for (let i = 0; i + 5 <= t.length; i++) {
|
|
if (t[i] !== 0xe8 && t[i] !== 0xe9) continue;
|
|
const target = text.vaddr + i + 5 + t.readInt32LE(i + 1);
|
|
const imp = wanted.get(target);
|
|
if (imp) hits.push({ site: text.vaddr + i, kind: t[i] === 0xe8 ? "call" : "jmp", name: imp.name });
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
const hex = (n) => "0x" + n.toString(16);
|
|
const [cmd, path, a, b] = process.argv.slice(2);
|
|
if (cmd === "unwrap") {
|
|
const elf = loadElf(path);
|
|
writeFileSync(a, unwrap(elf));
|
|
for (const ph of elf.phdrs) {
|
|
console.log(`phdr ${ph.index} type=${hex(ph.type)} flags=${ph.flags} vaddr=${hex(ph.vaddr)} filesz=${hex(ph.filesz)} memsz=${hex(ph.memsz)} off=${hex(ph.offset)}`);
|
|
}
|
|
} else if (cmd === "imports") {
|
|
const re = a ? new RegExp(a) : /./;
|
|
const { list } = imports(loadElf(path));
|
|
for (const i of list.filter((i) => re.test(i.name)).sort((x, y) => (x.name < y.name ? -1 : 1))) {
|
|
console.log(`${i.kind} got=${hex(i.slot)} stub=${i.stub === undefined ? "-" : hex(i.stub)} ${i.name}`);
|
|
}
|
|
} else if (cmd === "callers") {
|
|
for (const h of callers(loadElf(path), new RegExp(a))) console.log(`${hex(h.site)} ${h.kind} ${h.name}`);
|
|
} else if (cmd === "bytes") {
|
|
const buf = readVaddr(loadElf(path), Number(a), Number(b));
|
|
console.log(buf ? buf.toString("hex") : "unmapped");
|
|
} else if (cmd) {
|
|
console.error("unknown command");
|
|
process.exit(2);
|
|
}
|