Files
djhackersdev_bemanitools/src/main/ezusb-emu/device.c
T

422 lines
12 KiB
C

#define LOG_MODULE "ezusb-emu-device"
// clang-format off
// Don't format because the order is important here
#include <windows.h>
#include <setupapi.h>
#include <usb100.h>
// clang-format on
#include <string.h>
#include "ezusb/ezusbsys2.h"
#include "ezusb/util.h"
#include "ezusb-emu/conf.h"
#include "ezusb-emu/desc.h"
#include "ezusb-emu/device.h"
#include "ezusb-emu/msg.h"
#include "ezusb-emu/util.h"
#include "hook/iohook.h"
#include "imports/avs.h"
#include "util/fs.h"
#include "util/hex.h"
#include "util/iobuf.h"
#include "util/log.h"
#include "util/str.h"
// The max buffer size in iidx's ezusb client library is 4096 for the initial
// firmware download.
#define MAX_IOCTL_BUFFER_SIZE 4096
static HRESULT ezusb_get_device_descriptor(struct irp *irp);
static HRESULT ezusb_vendor_req(struct irp *irp);
static HRESULT ezusb_upload_fw(struct irp *irp);
static HRESULT ezusb_pipe_read(struct irp *irp);
static HRESULT ezusb_pipe_write(struct irp *irp);
static HRESULT ezusb_open(struct irp *irp);
static HRESULT ezusb_ioctl(struct irp *irp);
enum ezusb_pipe {
/* This is just the NT driver API. Add 1 to get the actual EP number. */
EZUSB_PIPE_INTERRUPT_OUT = 0,
EZUSB_PIPE_INTERRUPT_IN = 1,
EZUSB_PIPE_BULK_OUT = 2,
EZUSB_PIPE_BULK_IN = 3
};
static HANDLE ezusb_emu_fd;
static struct ezusb_firmware *ezusb_emu_firmware;
static struct ezusb_emu_msg_hook *ezusb_emu_dev_fx_msg_hook;
void ezusb_emu_device_hook_init(struct ezusb_emu_msg_hook *msg_hook)
{
log_assert(ezusb_emu_fd == NULL);
HRESULT hr;
hr = iohook_open_nul_fd(&ezusb_emu_fd);
if (hr != S_OK) {
log_fatal("Opening nul fd failed: %08lx", hr);
}
ezusb_emu_dev_fx_msg_hook = msg_hook;
}
void ezusb_emu_device_hook_fini(void)
{
if (ezusb_emu_fd != NULL) {
CloseHandle(ezusb_emu_fd);
}
ezusb_emu_fd = NULL;
}
HRESULT
ezusb_emu_device_dispatch_irp(struct irp *irp)
{
if (irp->op != IRP_OP_OPEN && irp->fd != ezusb_emu_fd) {
return iohook_invoke_next(irp);
}
/* read/write are not supported, and the game-side EZUSB code constantly
churns through FDs opening and closing them (so we silently acknowledge
CloseHandle calls and don't even log them). */
switch (irp->op) {
case IRP_OP_OPEN:
return ezusb_open(irp);
case IRP_OP_CLOSE:
return S_OK;
case IRP_OP_IOCTL:
return ezusb_ioctl(irp);
default:
return E_NOTIMPL;
}
}
/*
* WIN32 I/O AND IOHOOK LAYER
*/
static HRESULT ezusb_open(struct irp *irp)
{
log_assert(irp != NULL);
if (!wstr_eq(irp->open_filename, L"\\\\.\\Ezusb-0")) {
return iohook_invoke_next(irp);
}
irp->fd = ezusb_emu_fd;
return S_OK;
}
static HRESULT ezusb_ioctl(struct irp *irp)
{
HRESULT res;
// Stack alloc'd and fixed sized buffers to avoid processing costs with
// allocations. Ensure buffers are large enough for any operation
uint8_t write_buffer_local[MAX_IOCTL_BUFFER_SIZE];
uint8_t read_buffer_local[MAX_IOCTL_BUFFER_SIZE];
uint8_t *write_buffer_orig;
uint8_t *read_buffer_orig;
// Save original buffer that is owned and managed by the caller/the game
// Do NOT read/write these buffers directly because the game's ezusb
// interface library does not access to them entirely thread safe.
// This causes verious odd bugs due to data read/write inconsistencies
// between data access and modification by at least two different threads.
//
// The game's ezusb (client) library was created on a platform with no
// true multi-core processing (Pentium 4 era of hardware). However, the
// developers utilized threading primitives of the Win32 API to ensure
// a high rate of data updates to reduce input latency. This was architected
// using a dedicated polling thread in the ezusb client library that drives
// the IO polling on a high update rate. The captured data was stored in
// a shared buffer that is also accessible by other threads from the main
// game binary. However, the data access to the same shared buffer with the
// polling backend is not synchronized
//
// Thus, the various odd and flaky ezusb communication bugs we see on
// modern, and true multi-core hardware, are the concurrency management
// mistakes that are now creeping up. The developers back then had no
// proper means to test these due to the lack of hardware capabilities.
// Save the IO buffer that is used by the ezusb client backend and shared
// with the game's main thread
write_buffer_orig = (uint8_t *) irp->write.bytes;
read_buffer_orig = irp->read.bytes;
// Prepare our own thread locally managed and non shared buffers for any
// further data operations that are part of the ezusb emulation stack
memset(write_buffer_local, 0, sizeof(write_buffer_local));
memset(read_buffer_local, 0, sizeof(read_buffer_local));
// Sanity check and visibility, in case this ever overflows
if (irp->write.nbytes > sizeof(write_buffer_local)) {
log_fatal(
"Insufficient local write buffer available for ioctl, local "
"size %d, ioctl buffer size %d",
sizeof(write_buffer_local),
irp->write.nbytes);
}
if (irp->read.nbytes > sizeof(read_buffer_local)) {
log_fatal(
"Insufficient local read buffer available for ioctl, local "
"size %d, ioctl buffer size %d",
sizeof(read_buffer_local),
irp->read.nbytes);
}
// Temporarily hook our local buffers to the irp
irp->write.bytes = write_buffer_local;
irp->read.bytes = read_buffer_local;
// Move data from the shared buffers to the local one
// Probably the "most atomic way possible" to have the least amount of
// overlap with the game's shared buffer
memcpy(write_buffer_local, write_buffer_orig, irp->write.nbytes);
memcpy(read_buffer_local, read_buffer_orig, irp->read.nbytes);
/* For debugging */
#ifdef EZUSB_EMU_DEBUG_DUMP
/* For debugging */
ezusb_emu_util_log_usb_msg(
"BEFORE",
irp->ioctl,
read_buffered.bytes,
read_buffered.nbytes,
read_buffered.bytes,
read_buffered.nbytes,
write_buffered.bytes,
write_buffered.nbytes);
#endif
/* Cases are listed in order of first receipt */
switch (irp->ioctl) {
case IOCTL_Ezusb_GET_DEVICE_DESCRIPTOR:
res = ezusb_get_device_descriptor(irp);
break;
case IOCTL_Ezusb_VENDOR_REQUEST:
res = ezusb_vendor_req(irp);
break;
case IOCTL_EZUSB_ANCHOR_DOWNLOAD:
res = ezusb_upload_fw(irp);
break;
case IOCTL_EZUSB_BULK_READ:
/* Misnomer: can be bulk or interrupt. */
res = ezusb_pipe_read(irp);
break;
case IOCTL_EZUSB_BULK_WRITE:
/* Ditto. */
res = ezusb_pipe_write(irp);
break;
default:
log_warning("Unknown ioctl %08x", irp->ioctl);
res = E_INVALIDARG;
}
#ifdef EZUSB_EMU_DEBUG_DUMP
/* For debugging */
ezusb_emu_util_log_usb_msg(
"AFTER",
irp->ioctl,
read_buffered.bytes,
read_buffered.nbytes,
read_buffered.bytes,
read_buffered.nbytes,
write_buffered.bytes,
write_buffered.nbytes);
#endif
// Move data back to shared IO buffer. Again, keep this keeps the access
// overlap as minimal as possible
memcpy(write_buffer_orig, write_buffer_local, irp->write.nbytes);
memcpy(read_buffer_orig, read_buffer_local, irp->read.nbytes);
// Re-store the original irp buffer state
irp->write.bytes = (const uint8_t *) write_buffer_orig;
irp->read.bytes = read_buffer_orig;
return res;
}
/*
* USB TRANSFER LAYER
*/
static HRESULT ezusb_get_device_descriptor(struct irp *irp)
{
USB_DEVICE_DESCRIPTOR *desc;
log_assert(irp != NULL);
if (irp->read.nbytes < sizeof(*desc)) {
log_warning("USB_DEVICE_DESCRIPTOR buffer too small");
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
desc = (USB_DEVICE_DESCRIPTOR *) irp->read.bytes;
memset(desc, 0, sizeof(*desc));
desc->idVendor = ezusb_emu_desc_device.vid;
desc->idProduct = ezusb_emu_desc_device.pid;
irp->read.pos = sizeof(*desc);
log_misc(
"get_device_descriptor: vid %02x, pid %02x",
desc->idVendor,
desc->idProduct);
return S_OK;
}
static HRESULT ezusb_vendor_req(struct irp *irp)
{
VENDOR_OR_CLASS_REQUEST_CONTROL *vc;
log_assert(irp != NULL);
if (irp->write.nbytes < sizeof(*vc)) {
log_warning("VENDOR_OR_CLASS_REQUEST_CONTROL buffer too small");
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
vc = (VENDOR_OR_CLASS_REQUEST_CONTROL *) irp->write.bytes;
log_misc(
"vendor req %02x, value %04x, index %04x",
vc->request,
vc->value,
vc->index);
if (vc->request == 0x00 && vc->value == 0x0001 && vc->index == 0x0100) {
log_misc("vendor req: reset hold, starting fw download...");
ezusb_emu_firmware = ezusb_firmware_alloc();
} else if (
vc->request == 0x00 && vc->value == 0x0001 && vc->index == 0x0000) {
log_misc("vendor req: reset release, finished fw download");
/* FW download finished, reset 8051 and start the downloaded FW */
ezusb_emu_firmware->crc = ezusb_firmware_crc(ezusb_emu_firmware);
#ifdef EZUSB_EMU_FW_DUMP
if (!ezusb_firmware_save("ezusb_fx.bin", ezusb_emu_firmware)) {
log_fatal("Saving dumped firmware failed");
} else {
log_misc("firmware dumped do ezusb_fx.bin file");
}
#endif
free(ezusb_emu_firmware);
ezusb_emu_firmware = NULL;
} else {
log_warning("VENDOR_OR_CLASS_REQUEST_CONTROL unknown request");
}
return S_OK;
}
static HRESULT ezusb_upload_fw(struct irp *irp)
{
ANCHOR_DOWNLOAD_CONTROL *hdr;
log_assert(irp != NULL);
if (irp->write.nbytes < sizeof(*hdr)) {
log_warning("ANCHOR_DOWNLOAD_CONTROL buffer too small");
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
hdr = (ANCHOR_DOWNLOAD_CONTROL *) irp->write.bytes;
/* The semantics for this ioctl are FUCKED! The second buffer is
supposed to receive data, not send it! */
log_misc(
"upload_fw: offset: %04x, nbytes: %04x", hdr->Offset, irp->read.nbytes);
ezusb_firmware_add_segment(
ezusb_emu_firmware,
ezusb_firmware_segment_alloc(
hdr->Offset, irp->read.nbytes, (void *) irp->read.bytes));
return S_OK;
}
static HRESULT ezusb_pipe_read(struct irp *irp)
{
BULK_TRANSFER_CONTROL *ctl;
log_assert(irp != NULL);
if (irp->write.nbytes < sizeof(*ctl)) {
log_warning("BULK_TRANSFER_CONTROL buffer too small");
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
ctl = (BULK_TRANSFER_CONTROL *) irp->write.bytes;
switch (ctl->pipeNum) {
case EZUSB_PIPE_INTERRUPT_IN:
return ezusb_emu_dev_fx_msg_hook->interrupt_read(&irp->read);
case EZUSB_PIPE_BULK_IN:
return ezusb_emu_dev_fx_msg_hook->bulk_read(&irp->read);
default:
log_warning("No such read pipe: %u", (unsigned int) ctl->pipeNum);
return E_INVALIDARG;
}
}
static HRESULT ezusb_pipe_write(struct irp *irp)
{
BULK_TRANSFER_CONTROL *ctl;
struct const_iobuf write;
if (irp->write.nbytes < sizeof(*ctl)) {
log_warning("BULK_TRANSFER_CONTROL buffer too small");
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
ctl = (BULK_TRANSFER_CONTROL *) irp->write.bytes;
/* ugh */
write.bytes = irp->read.bytes;
write.nbytes = irp->read.nbytes;
write.pos = 0;
switch (ctl->pipeNum) {
case EZUSB_PIPE_INTERRUPT_OUT:
return ezusb_emu_dev_fx_msg_hook->interrupt_write(&write);
case EZUSB_PIPE_BULK_OUT:
return ezusb_emu_dev_fx_msg_hook->bulk_write(&write);
default:
log_warning("No such write pipe: %u", (unsigned int) ctl->pipeNum);
return E_INVALIDARG;
}
}