#define LOG_MODULE "ezusb2-emu-device" // clang-format off // Don't format because the order is important here #include #include #include // clang-format on #include #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; }