Files
project-solana/games/rac1/host/src/platform_win32.c
T
2026-10-14 12:49:56 -04:00

854 lines
38 KiB
C

/* 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 <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <shellapi.h>
#include <mmsystem.h>
#include <string.h>
#include <ctype.h>
extern int plat_gl_active; /* the GS presents with OpenGL (below) */
/* Something other than the game holds a PS2 address: say what, and stop (taking the memory
* over would break whatever owns it: once it was this DLL's own code, relocated there). */
static int map_conflict(uint32_t addr, uint32_t size, uint32_t at, const MEMORY_BASIC_INFORMATION *mi)
{
char who[MAX_PATH] = "memory of another program loaded into the game (an overlay or similar)", msg[1024];
if (mi && mi->Type == MEM_IMAGE && GetModuleFileNameA((HMODULE)mi->AllocationBase, who, sizeof who)) {}
uya_log("cannot map PS2 memory 0x%08X+0x%X: 0x%08X is in use by %s", addr, size, at, who);
snprintf(msg, sizeof msg, "The game needs the memory at 0x%08X for the PS2, but it is in use by:\n\n%s\n\n"
"If this is a program injected into games (an overlay, recorder or RGB tool), close it or exclude "
"rac1_wrapper.exe from it, then try again.", at, who);
MessageBoxA(0, msg, "Project Solana", MB_ICONERROR);
return -1;
}
int plat_map_fixed(uint32_t addr, uint32_t size, int exec)
{
DWORD prot = exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
static int said;
if (!said++) { /* (normally 0x40000000; elsewhere when Windows relocated it) */
MEMORY_BASIC_INFORMATION me;
if (VirtualQuery((void *)plat_map_fixed, &me, sizeof me)) uya_log("rac1_game.dll loaded at %p", me.AllocationBase);
if ((uintptr_t)me.AllocationBase < 0x40000000u) { /* (rac1_wrapper.exe keeps it above: see launcher_win32.c) */
char msg[512];
snprintf(msg, sizeof msg, "rac1_game.dll was loaded at 0x%08X, but the game needs it at 0x40000000 or above "
"(lower addresses belong to the PS2). Please report this, with rac1_log.txt.", (unsigned)(uintptr_t)me.AllocationBase);
uya_log("%s", msg);
MessageBoxA(0, msg, "Project Solana", MB_ICONERROR);
return -1;
}
}
MEMORY_BASIC_INFORMATION mi;
void *p = 0;
if (VirtualQuery((void *)(uintptr_t)addr, &mi, sizeof mi)) {
if (mi.State == MEM_RESERVE && mi.AllocationBase == (void *)(uintptr_t)addr && mi.RegionSize >= size)
p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_COMMIT, prot); /* rac1_wrapper.exe reserved it before loading this DLL */
else if (mi.State == MEM_FREE)
p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_RESERVE | MEM_COMMIT, prot);
}
if (p == (void *)(uintptr_t)addr) return 0;
/* already there: only PS2 RAM, which is the wrapper's own image section (.ps2ram) */
HMODULE self = GetModuleHandleA(0);
uint32_t a = addr;
while (a < addr + size) {
if (!VirtualQuery((void *)(uintptr_t)a, &mi, sizeof mi)) return map_conflict(addr, size, a, 0);
if (mi.State != MEM_COMMIT || mi.AllocationBase != (void *)self) return map_conflict(addr, size, a, &mi);
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();
}
}
/* the function at rva in rac1_game.sym (written by build.py: "RVA name" per line, sorted),
* read with Win32 calls only: the C library may be what crashed */
static int sym_lookup(uint32_t rva, char *name, int cap, uint32_t *start)
{
char p[MAX_PATH + 32];
DWORD n = GetModuleFileNameA(0, p, MAX_PATH);
while (n && p[n - 1] != '\\') n--;
lstrcpyA(p + n, "rac1_game.sym");
HANDLE f = CreateFileA(p, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
if (f == INVALID_HANDLE_VALUE) return 0;
DWORD sz = GetFileSize(f, 0), got = 0;
char *b = sz && sz < (64u << 20) ? VirtualAlloc(0, sz + 1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) : 0;
int ok = b && ReadFile(f, b, sz, &got, 0) && got == sz;
CloseHandle(f);
if (!ok) { if (b) VirtualFree(b, 0, MEM_RELEASE); return 0; }
b[sz] = '\n';
const char *best = 0; uint32_t ba = 0;
for (char *q = b; q < b + sz;) {
uint32_t a = 0; char *s = q;
for (; s < q + 8; s++) a = a * 16 + (uint32_t)(*s <= '9' ? *s - '0' : (*s | 32) - 'a' + 10);
if (a > rva) break;
best = s + 1; ba = a;
while (*q != '\n') q++;
q++;
}
int k = 0;
if (best) for (; best[k] != '\n' && best[k] != '\r' && k < cap - 1; k++) name[k] = best[k];
name[k] = 0; *start = ba;
VirtualFree(b, 0, MEM_RELEASE);
return k > 0;
}
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;
}
static volatile LONG in_crash;
if (InterlockedExchange(&in_crash, 1)) ExitProcess(1); /* (a crash while reporting one) */
{ /* where: module+offset, the form crash_lookup.py and Windows' event log use */
char mod[MAX_PATH] = "?"; MEMORY_BASIC_INFORMATION mi;
uintptr_t base = 0;
if (VirtualQuery(er->ExceptionAddress, &mi, sizeof mi) && mi.Type == MEM_IMAGE) {
base = (uintptr_t)mi.AllocationBase;
if (GetModuleFileNameA((HMODULE)base, mod, sizeof mod)) { char *s = strrchr(mod, '\\'); if (s) memmove(mod, s + 1, strlen(s)); }
}
uint32_t off = (uint32_t)((uintptr_t)er->ExceptionAddress - base);
uya_log("crash: exception 0x%08lX at 0x%08X (%s+0x%X), fault address 0x%08lX", er->ExceptionCode, at, base ? mod : "no module",
off, er->NumberParameters > 1 ? (unsigned long)er->ExceptionInformation[1] : 0ul);
char fn[256]; uint32_t fs;
MEMORY_BASIC_INFORMATION me; /* (this DLL: is the crash in it?) */
if (base && VirtualQuery((void *)sym_lookup, &me, sizeof me) && me.AllocationBase == (void *)base && sym_lookup(off, fn, sizeof fn, &fs))
uya_log("crash: in %s+0x%X", fn, off - fs);
/* the return addresses on the stack, named: the call chain */
const uint32_t *sp = (const uint32_t *)(uintptr_t)c->Esp;
for (int i = 0, shown = 0; i < 256 && shown < 12 && !IsBadReadPtr(sp + i, 4); i++) {
uintptr_t v = sp[i];
if (base && v > base && v < base + 0x10000000 && VirtualQuery((void *)v, &me, sizeof me) && me.AllocationBase == (void *)base &&
(me.Protect & 0xF0) && sym_lookup((uint32_t)(v - base), fn, sizeof fn, &fs)) {
uya_log("crash: called from %s+0x%X", fn, (uint32_t)(v - base) - fs); shown++;
}
}
}
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;
}
#include <signal.h>
/* abort() (a library giving up) ends the process without an exception: raise one, so the
* crash handler reports it with the call chain */
static void on_abort(int sig)
{
(void)sig;
uya_log("crash: abort() was called");
RaiseException(0xE0000ABE, EXCEPTION_NONCONTINUABLE, 0, 0);
}
int plat_install_trap_handler(void)
{
signal(SIGABRT, on_abort);
return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1;
}
void plat_log_system(void)
{
typedef struct { ULONG size, major, minor, build, platform; WCHAR sp[128]; } osv_t;
HMODULE nt = GetModuleHandleA("ntdll.dll");
LONG (WINAPI *rtl_ver)(osv_t *) = nt ? (void *)GetProcAddress(nt, "RtlGetVersion") : 0;
const char *(*wine_ver)(void) = nt ? (void *)GetProcAddress(nt, "wine_get_version") : 0;
osv_t v; memset(&v, 0, sizeof v); v.size = sizeof v;
if (rtl_ver) rtl_ver(&v);
SYSTEM_INFO si; GetNativeSystemInfo(&si);
MEMORYSTATUSEX ms; memset(&ms, 0, sizeof ms); ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms);
char cpu[128] = "?"; DWORD cl = sizeof cpu;
RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, 0, cpu, &cl);
uya_log("system: Windows %lu.%lu build %lu%s%s, %s, %lu cores, %llu MB RAM", v.major, v.minor, v.build,
wine_ver ? ", Wine " : "", wine_ver ? wine_ver() : "", cpu, si.dwNumberOfProcessors, (unsigned long long)(ms.ullTotalPhys >> 20));
}
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