465 lines
13 KiB
C
465 lines
13 KiB
C
#define LOG_MODULE "ezusb2-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/util.h"
|
|
|
|
#include "ezusb2/cyioctl.h"
|
|
|
|
#include "ezusb-emu/msg.h"
|
|
|
|
#include "ezusb2-emu/conf.h"
|
|
#include "ezusb2-emu/desc.h"
|
|
#include "ezusb2-emu/device.h"
|
|
#include "ezusb2-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
|
|
|
|
enum pipe {
|
|
PIPE_INT_OUT = 0x01,
|
|
PIPE_BULK_OUT = 0x02,
|
|
PIPE_INT_IN = 0x81,
|
|
/* yes, 0x86 and NOT 0x82... */
|
|
PIPE_BULK_IN = 0x86
|
|
};
|
|
|
|
struct ezusb_usb_string_desc {
|
|
uint8_t length;
|
|
uint8_t desc_type;
|
|
/* Max length? 9 elements set by game... */
|
|
uint16_t unicode_str[9];
|
|
};
|
|
|
|
static HRESULT ezusb_open(struct irp *irp);
|
|
static HRESULT ezusb_ioctl(struct irp *irp);
|
|
|
|
static HRESULT ezusb_ioctl_ep0(
|
|
SINGLE_TRANSFER *usb_req, struct const_iobuf *write, struct iobuf *read);
|
|
|
|
static HRESULT ezusb_ioctl_epX(
|
|
SINGLE_TRANSFER *usb_req, struct const_iobuf *write, struct iobuf *read);
|
|
|
|
static HRESULT ezusb_get_device_descriptor(struct iobuf *read);
|
|
static HRESULT ezusb_get_string_descriptor(struct iobuf *read);
|
|
static HRESULT ezusb_reset(struct const_iobuf *write);
|
|
static HRESULT ezusb_upload_fw(uint16_t offset, struct const_iobuf *write);
|
|
|
|
static HANDLE ezusb_fd;
|
|
static struct ezusb_firmware *ezusb_emu_firmware;
|
|
static struct ezusb_emu_msg_hook *ezusb_emu_dev_fx2_msg_hook;
|
|
|
|
void ezusb2_emu_device_hook_init(struct ezusb_emu_msg_hook *msg_hook)
|
|
{
|
|
log_assert(ezusb_fd == NULL);
|
|
|
|
HRESULT hr;
|
|
|
|
hr = iohook_open_nul_fd(&ezusb_fd);
|
|
|
|
if (hr != S_OK) {
|
|
log_fatal("Opening nul fd failed: %08lx", hr);
|
|
}
|
|
|
|
ezusb_emu_dev_fx2_msg_hook = msg_hook;
|
|
}
|
|
|
|
void ezusb2_emu_device_hook_fini(void)
|
|
{
|
|
if (ezusb_fd != NULL) {
|
|
CloseHandle(ezusb_fd);
|
|
}
|
|
|
|
ezusb_fd = NULL;
|
|
}
|
|
|
|
/*
|
|
* WIN32 I/O AND IOHOOK LAYER
|
|
*/
|
|
|
|
HRESULT
|
|
ezusb2_emu_device_dispatch_irp(struct irp *irp)
|
|
{
|
|
if (irp->op != IRP_OP_OPEN && irp->fd != ezusb_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;
|
|
}
|
|
}
|
|
|
|
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_fd;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT ezusb_ioctl(struct irp *irp)
|
|
{
|
|
SINGLE_TRANSFER *usb_req;
|
|
struct const_iobuf write;
|
|
struct iobuf read;
|
|
HRESULT hr;
|
|
|
|
// 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;
|
|
|
|
log_assert(irp != NULL);
|
|
|
|
// 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);
|
|
|
|
#ifdef EZUSB_EMU_DEBUG_DUMP
|
|
/* For debugging */
|
|
ezusb2_emu_util_log_usb_msg("BEFORE", irp);
|
|
#endif
|
|
|
|
if (irp->write.nbytes < sizeof(SINGLE_TRANSFER)) {
|
|
log_warning("SINGLE_TRANSFER out buffer too small");
|
|
|
|
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
|
|
}
|
|
|
|
if (irp->read.nbytes < sizeof(SINGLE_TRANSFER)) {
|
|
log_warning("SINGLE_TRANSFER in buffer too small");
|
|
|
|
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
|
|
}
|
|
|
|
memcpy(irp->read.bytes, irp->write.bytes, sizeof(SINGLE_TRANSFER));
|
|
|
|
write.bytes = irp->write.bytes + sizeof(SINGLE_TRANSFER);
|
|
write.nbytes = irp->write.nbytes - sizeof(SINGLE_TRANSFER);
|
|
write.pos = 0;
|
|
read.bytes = irp->read.bytes + sizeof(SINGLE_TRANSFER);
|
|
read.nbytes = irp->read.nbytes - sizeof(SINGLE_TRANSFER);
|
|
read.pos = 0;
|
|
|
|
usb_req = (SINGLE_TRANSFER *) irp->write.bytes;
|
|
|
|
switch (irp->ioctl) {
|
|
case IOCTL_ADAPT_SEND_EP0_CONTROL_TRANSFER:
|
|
hr = ezusb_ioctl_ep0(usb_req, &write, &read);
|
|
|
|
break;
|
|
|
|
case IOCTL_ADAPT_SEND_NON_EP0_TRANSFER:
|
|
hr = ezusb_ioctl_epX(usb_req, &write, &read);
|
|
|
|
break;
|
|
|
|
default:
|
|
log_warning("Unknown ioctl %08x", irp->ioctl);
|
|
hr = E_INVALIDARG;
|
|
|
|
break;
|
|
}
|
|
|
|
if (FAILED(hr)) {
|
|
return hr;
|
|
}
|
|
|
|
irp->read.pos = read.pos + sizeof(SINGLE_TRANSFER);
|
|
|
|
#ifdef EZUSB_EMU_DEBUG_DUMP
|
|
/* For debugging */
|
|
ezusb2_emu_util_log_usb_msg("AFTER", irp);
|
|
#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 hr;
|
|
}
|
|
|
|
/*
|
|
* USB TRANSFER LAYER
|
|
*/
|
|
|
|
static HRESULT ezusb_ioctl_ep0(
|
|
SINGLE_TRANSFER *usb_req, struct const_iobuf *write, struct iobuf *read)
|
|
{
|
|
log_assert(usb_req != NULL);
|
|
log_assert(write != NULL);
|
|
log_assert(read != NULL);
|
|
|
|
/*
|
|
0x80: Device -> Host Request, Standard, Recipient Device
|
|
0x40: Host -> Device Request, Class, Recipient Device
|
|
*/
|
|
|
|
if (usb_req->SetupPacket.bmRequest == 0x80 &&
|
|
usb_req->SetupPacket.bRequest == 0x06) {
|
|
switch (usb_req->SetupPacket.wValue) {
|
|
/* Get usb device descriptor */
|
|
case 0x0100:
|
|
/* return data with USB_DEVICE_DESCRIPTOR struct */
|
|
return ezusb_get_device_descriptor(read);
|
|
|
|
/* get usb string descriptor */
|
|
case 0x0301:
|
|
return ezusb_get_string_descriptor(read);
|
|
|
|
default:
|
|
log_warning(
|
|
"Unsupported standard req (dev->host) value: %04X",
|
|
usb_req->SetupPacket.wValue);
|
|
|
|
return E_INVALIDARG;
|
|
}
|
|
} else if (
|
|
usb_req->SetupPacket.bmRequest == 0x40 &&
|
|
usb_req->SetupPacket.bRequest == 0xA0) {
|
|
if (usb_req->SetupPacket.wValue == 0xE600) {
|
|
/* reset */
|
|
return ezusb_reset(write);
|
|
} else {
|
|
/* fw download */
|
|
return ezusb_upload_fw(usb_req->SetupPacket.wValue, write);
|
|
}
|
|
} else {
|
|
log_warning(
|
|
"Invalid standard req_type and/or request: %02X %02X",
|
|
usb_req->SetupPacket.bmRequest,
|
|
usb_req->SetupPacket.bRequest);
|
|
|
|
return E_INVALIDARG;
|
|
}
|
|
}
|
|
|
|
static HRESULT ezusb_ioctl_epX(
|
|
SINGLE_TRANSFER *usb_req, struct const_iobuf *write, struct iobuf *read)
|
|
{
|
|
log_assert(usb_req != NULL);
|
|
log_assert(write != NULL);
|
|
log_assert(read != NULL);
|
|
|
|
/* No reason to follow a standard, right? */
|
|
switch (usb_req->ucEndpointAddress) {
|
|
case PIPE_INT_OUT:
|
|
return ezusb_emu_dev_fx2_msg_hook->interrupt_write(write);
|
|
|
|
case PIPE_BULK_OUT:
|
|
return ezusb_emu_dev_fx2_msg_hook->bulk_write(write);
|
|
|
|
case PIPE_INT_IN:
|
|
return ezusb_emu_dev_fx2_msg_hook->interrupt_read(read);
|
|
|
|
case PIPE_BULK_IN:
|
|
return ezusb_emu_dev_fx2_msg_hook->bulk_read(read);
|
|
|
|
default:
|
|
log_warning("Unhandled endpoint %02X", usb_req->ucEndpointAddress);
|
|
|
|
return E_INVALIDARG;
|
|
}
|
|
}
|
|
|
|
static HRESULT ezusb_get_device_descriptor(struct iobuf *read)
|
|
{
|
|
USB_DEVICE_DESCRIPTOR desc;
|
|
|
|
log_assert(read != NULL);
|
|
|
|
if (read->nbytes < sizeof(desc)) {
|
|
log_warning("USB_DEVICE_DESCRIPTOR buffer too small: %d", read->nbytes);
|
|
|
|
return HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
|
|
}
|
|
|
|
memset(&desc, 0, sizeof(desc));
|
|
|
|
/* vid and pid checked, only */
|
|
desc.idVendor = ezusb2_emu_desc_device.vid;
|
|
desc.idProduct = ezusb2_emu_desc_device.pid;
|
|
|
|
log_misc(
|
|
"get_device_descriptor: vid %02x, pid %02x",
|
|
desc.idVendor,
|
|
desc.idProduct);
|
|
|
|
// Single write to external/game managed buffer to reduce risk for
|
|
// inconsistent state
|
|
memcpy(read->bytes, &desc, sizeof(desc));
|
|
read->pos = sizeof(desc);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT ezusb_get_string_descriptor(struct iobuf *read)
|
|
{
|
|
struct ezusb_usb_string_desc desc;
|
|
|
|
log_assert(read != NULL);
|
|
|
|
if (read->nbytes < sizeof(desc)) {
|
|
log_warning("ezusb_usb_string_desc buffer too small: %d", read->nbytes);
|
|
|
|
return HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
|
|
}
|
|
|
|
memset(&desc, 0, sizeof(desc));
|
|
|
|
desc.length = sizeof(desc);
|
|
desc.desc_type = 0x03; /* Usb spec says so */
|
|
memcpy(desc.unicode_str, L"KONAMI", 12); /* Unicode encoding */
|
|
|
|
// Single write to external/game managed buffer to reduce risk for
|
|
// inconsistent state
|
|
memcpy(read->bytes, &desc, sizeof(desc));
|
|
read->pos = sizeof(desc);
|
|
|
|
log_misc("get_string_descriptor: KONAMI");
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT ezusb_reset(struct const_iobuf *write)
|
|
{
|
|
log_assert(write != NULL);
|
|
|
|
switch (write->bytes[0]) {
|
|
case 0x01:
|
|
log_misc("reset hold, starting fw download...");
|
|
ezusb_emu_firmware = ezusb_firmware_alloc();
|
|
|
|
return S_OK;
|
|
|
|
case 0x00:
|
|
log_misc("reset release, finished fw download");
|
|
|
|
ezusb_emu_firmware->crc = ezusb_firmware_crc(ezusb_emu_firmware);
|
|
|
|
#ifdef EZUSB_EMU_FW_DUMP
|
|
if (!ezusb_firmware_save("ezusb_fx2.bin", ezusb_emu_firmware)) {
|
|
log_fatal("Saving dumped firmware failed");
|
|
} else {
|
|
log_misc("firmware dumped do ezusb_fx2.bin file");
|
|
}
|
|
|
|
free(ezusb_emu_firmware);
|
|
ezusb_emu_firmware = NULL;
|
|
#endif
|
|
|
|
return S_OK;
|
|
|
|
default:
|
|
log_warning("Unknown reset cmd: %02X", write->bytes[0]);
|
|
|
|
return E_FAIL;
|
|
}
|
|
}
|
|
|
|
static HRESULT ezusb_upload_fw(uint16_t offset, struct const_iobuf *write)
|
|
{
|
|
log_misc("upload_fw, offset %04X, nbytes %04X", offset, write->nbytes);
|
|
|
|
ezusb_firmware_add_segment(
|
|
ezusb_emu_firmware,
|
|
ezusb_firmware_segment_alloc(
|
|
offset, write->nbytes, (void *) write->bytes));
|
|
|
|
return S_OK;
|
|
}
|