281 lines
10 KiB
C
281 lines
10 KiB
C
/* 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 <sys/mman.h>
|
|
#include <signal.h>
|
|
#include <string.h>
|
|
#include <ucontext.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
#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 <time.h>
|
|
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 <sys/time.h>
|
|
#include <sys/syscall.h>
|
|
#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 <stdlib.h>
|
|
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
|