854 lines
38 KiB
C
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
|