diff --git a/src/main/ezusb-emu/device.c b/src/main/ezusb-emu/device.c index 4a2c28c..0773f28 100644 --- a/src/main/ezusb-emu/device.c +++ b/src/main/ezusb-emu/device.c @@ -28,6 +28,10 @@ #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); @@ -115,43 +119,114 @@ static HRESULT ezusb_open(struct irp *irp) 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, - irp->read.bytes, - irp->read.nbytes, - irp->read.bytes, - irp->read.nbytes, - irp->write.bytes, - irp->write.nbytes); + 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: - return ezusb_get_device_descriptor(irp); + res = ezusb_get_device_descriptor(irp); + break; case IOCTL_Ezusb_VENDOR_REQUEST: - return ezusb_vendor_req(irp); + res = ezusb_vendor_req(irp); + break; case IOCTL_EZUSB_ANCHOR_DOWNLOAD: - return ezusb_upload_fw(irp); + res = ezusb_upload_fw(irp); + break; case IOCTL_EZUSB_BULK_READ: /* Misnomer: can be bulk or interrupt. */ - return ezusb_pipe_read(irp); + res = ezusb_pipe_read(irp); + break; case IOCTL_EZUSB_BULK_WRITE: /* Ditto. */ - return ezusb_pipe_write(irp); + res = ezusb_pipe_write(irp); + break; default: log_warning("Unknown ioctl %08x", irp->ioctl); - return E_INVALIDARG; + res = E_INVALIDARG; } #ifdef EZUSB_EMU_DEBUG_DUMP @@ -159,13 +234,24 @@ static HRESULT ezusb_ioctl(struct irp *irp) ezusb_emu_util_log_usb_msg( "AFTER", irp->ioctl, - irp->read.bytes, - irp->read.nbytes, - irp->read.bytes, - irp->read.nbytes, - irp->write.bytes, - irp->write.nbytes); + 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