mirror of
https://github.com/djhackersdev/bemanitools.git
synced 2024-12-01 01:27:18 +01:00
feat(p3iodrv): Add DDR compatible P3IO driver
Initial version that supports the most important features to operate all inputs and outputs of a DDR SD cabinet.
This commit is contained in:
parent
1a86cc7ee5
commit
c22ec4fcff
@ -166,6 +166,7 @@ include src/main/launcher/Module.mk
|
|||||||
include src/main/mempatch-hook/Module.mk
|
include src/main/mempatch-hook/Module.mk
|
||||||
include src/main/mm/Module.mk
|
include src/main/mm/Module.mk
|
||||||
include src/main/p3io/Module.mk
|
include src/main/p3io/Module.mk
|
||||||
|
include src/main/p3iodrv/Module.mk
|
||||||
include src/main/p3ioemu/Module.mk
|
include src/main/p3ioemu/Module.mk
|
||||||
include src/main/p4iodrv/Module.mk
|
include src/main/p4iodrv/Module.mk
|
||||||
include src/main/p4ioemu/Module.mk
|
include src/main/p4ioemu/Module.mk
|
||||||
|
10
src/main/p3iodrv/Module.mk
Normal file
10
src/main/p3iodrv/Module.mk
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
libs += p3iodrv
|
||||||
|
|
||||||
|
libs_p4iodrv := \
|
||||||
|
p3io \
|
||||||
|
util \
|
||||||
|
|
||||||
|
src_p3iodrv := \
|
||||||
|
ddr.c \
|
||||||
|
device.c \
|
||||||
|
|
326
src/main/p3iodrv/ddr.c
Normal file
326
src/main/p3iodrv/ddr.c
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
#define LOG_MODULE "p3iodrv-ddr"
|
||||||
|
|
||||||
|
#include "p3io/cmd.h"
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
#include "ddr.h"
|
||||||
|
#include "device.h"
|
||||||
|
|
||||||
|
static uint8_t p3iodrv_ddr_seq_counter;
|
||||||
|
|
||||||
|
static uint8_t p3iodrv_ddr_get_and_update_seq_counter()
|
||||||
|
{
|
||||||
|
return p3iodrv_ddr_seq_counter++ & 0xF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HRESULT p3iodrv_ddr_check_resp(
|
||||||
|
const union p3io_resp_any *resp,
|
||||||
|
uint8_t expected_size,
|
||||||
|
const union p3io_req_any *corresponding_req)
|
||||||
|
{
|
||||||
|
uint8_t actual_size;
|
||||||
|
|
||||||
|
log_assert(resp);
|
||||||
|
|
||||||
|
actual_size = p3io_get_full_resp_size(resp);
|
||||||
|
|
||||||
|
if (actual_size != expected_size) {
|
||||||
|
log_warning(
|
||||||
|
"Incorrect response size, actual %d != expected %d",
|
||||||
|
actual_size,
|
||||||
|
expected_size);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_BAD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp->hdr.seq_no != corresponding_req->hdr.seq_no) {
|
||||||
|
log_warning(
|
||||||
|
"Incorrect sequence num in response, actual %d != expected %d",
|
||||||
|
resp->hdr.seq_no,
|
||||||
|
corresponding_req->hdr.seq_no);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp->hdr.cmd != corresponding_req->hdr.cmd) {
|
||||||
|
log_warning(
|
||||||
|
"Incorrect command in response, actual 0x%d != expected 0x%d",
|
||||||
|
resp->hdr.cmd,
|
||||||
|
corresponding_req->hdr.cmd);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_BAD_COMMAND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_init(HANDLE handle)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(&req.hdr, seq_cnt, P3IO_CMD_INIT, sizeof(req.init));
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.init), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.init.status != 0) {
|
||||||
|
log_warning("Initialization failed");
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_GEN_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_get_version(
|
||||||
|
HANDLE handle,
|
||||||
|
char str[4],
|
||||||
|
uint32_t *major,
|
||||||
|
uint32_t *minor,
|
||||||
|
uint32_t *patch)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(major);
|
||||||
|
log_assert(minor);
|
||||||
|
log_assert(patch);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr, seq_cnt, P3IO_CMD_GET_VERSION, sizeof(req.version));
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.version), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(str, resp.version.str, sizeof(char[4]));
|
||||||
|
*major = resp.version.major;
|
||||||
|
*minor = resp.version.minor;
|
||||||
|
*patch = resp.version.patch;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_set_watchdog(HANDLE handle, bool enable)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr, seq_cnt, P3IO_CMD_SET_WATCHDOG, sizeof(req.watchdog));
|
||||||
|
|
||||||
|
req.watchdog.enable = enable ? 1 : 0;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.watchdog), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_get_dipsw(HANDLE handle, uint8_t *state)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(state);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr,
|
||||||
|
seq_cnt,
|
||||||
|
P3IO_CMD_GET_CAB_TYPE_OR_DIPSW,
|
||||||
|
sizeof(req.cab_type_or_dipsw));
|
||||||
|
|
||||||
|
req.cab_type_or_dipsw.cab_type_or_dipsw = P3IO_DIP_SW_SELECTOR;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.cab_type_or_dipsw), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
*state = resp.cab_type_or_dipsw.state;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_get_cab_type(HANDLE handle, enum p3io_cab_type *type)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(type);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr,
|
||||||
|
seq_cnt,
|
||||||
|
P3IO_CMD_GET_CAB_TYPE_OR_DIPSW,
|
||||||
|
sizeof(req.cab_type_or_dipsw));
|
||||||
|
|
||||||
|
req.cab_type_or_dipsw.cab_type_or_dipsw = P3IO_CAB_TYPE_SELECTOR;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.cab_type_or_dipsw), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
*type = resp.cab_type_or_dipsw.state;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_get_video_freq(HANDLE handle, enum p3io_video_freq *freq)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(freq);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr, seq_cnt, P3IO_CMD_GET_VIDEO_FREQ, sizeof(req.video_freq));
|
||||||
|
|
||||||
|
// Must be set to 5 in order to return the right values. There might be more
|
||||||
|
// to this, e.g. this being some kind of selector where to read from the
|
||||||
|
// JAMMA harness (?) but this is good for now to get what we want to know
|
||||||
|
// for DDR
|
||||||
|
req.video_freq.unknown_05 = 5;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.video_freq), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
*freq = (enum p3io_video_freq) resp.video_freq.video_freq;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_ddr_get_jamma(HANDLE handle, struct p3io_ddr_jamma *jamma)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint32_t *jamma_raw;
|
||||||
|
|
||||||
|
jamma_raw = (uint32_t *) jamma;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_read_jamma(handle, jamma_raw);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inputs are active low for p1, p2 and operator bits
|
||||||
|
jamma_raw[0] ^= 0xFFFFFF00;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT
|
||||||
|
p3iodrv_ddr_set_outputs(HANDLE handle, const struct p3io_ddr_output *state)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t seq_cnt;
|
||||||
|
union p3io_req_any req;
|
||||||
|
union p3io_resp_any resp;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(state);
|
||||||
|
|
||||||
|
seq_cnt = p3iodrv_ddr_get_and_update_seq_counter();
|
||||||
|
|
||||||
|
p3io_req_hdr_init(
|
||||||
|
&req.hdr, seq_cnt, P3IO_CMD_SET_OUTPUTS, sizeof(req.set_outputs));
|
||||||
|
|
||||||
|
memcpy(&req.set_outputs.outputs, state, sizeof(req.set_outputs.outputs));
|
||||||
|
|
||||||
|
// Always set by the game like this
|
||||||
|
req.set_outputs.unk_FF = 0xFF;
|
||||||
|
|
||||||
|
hr = p3iodrv_device_transfer(handle, &req, &resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = p3iodrv_ddr_check_resp(&resp, sizeof(resp.set_outputs), &req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
115
src/main/p3iodrv/ddr.h
Normal file
115
src/main/p3iodrv/ddr.h
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#ifndef P3IODRV_DDR_H
|
||||||
|
#define P3IODRV_DDR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "p3io/cmd.h"
|
||||||
|
#include "p3io/ddr.h"
|
||||||
|
|
||||||
|
#define P3IODRV_DDR_VERSION_STR_LEN 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the p3io device for operation.
|
||||||
|
*
|
||||||
|
* This call is also somewhat referred to as "set mode" in the game, though it
|
||||||
|
* is part of a larger setup routine.
|
||||||
|
*
|
||||||
|
* If the p3io device is not initialized, the menu buttons (left, right, start)
|
||||||
|
* on (a SD) cabinet will periodically blink. Once this function is called, this
|
||||||
|
* blinking stops indicating the p3io device is initialized.
|
||||||
|
*
|
||||||
|
* Other p3io commands might work (partially) even if this function is not
|
||||||
|
* called. However, it is recommended to call this first after every p3io
|
||||||
|
* device reset.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_init(HANDLE handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the DDR specific version information from the p3io device.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param str Pointer to a buffer to write the version string to
|
||||||
|
* @param major Pointer to a uint32 to write the major version number to
|
||||||
|
* @param minor Pointer to a uint32 to write the minor version number to
|
||||||
|
* @param patch Poitner to a uint32 to ewrite the patch version number to
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_get_version(
|
||||||
|
HANDLE handle,
|
||||||
|
char str[P3IODRV_DDR_VERSION_STR_LEN],
|
||||||
|
uint32_t *major,
|
||||||
|
uint32_t *minor,
|
||||||
|
uint32_t *patch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable the device side watchdog. The watchdog will reset the device
|
||||||
|
* if it is not reset periodically.
|
||||||
|
*
|
||||||
|
* If you stop sending commands to the p3io, it will trigger after about 5-7
|
||||||
|
* seconds of not receiving anything. You can notice the watchdog triggered when
|
||||||
|
* the menu buttons start to blink again which indicates the device is not
|
||||||
|
* initialized (anymore).
|
||||||
|
*
|
||||||
|
* The watchdog seems to expect that you periodically send the "get version"
|
||||||
|
* command to reset it and keep the p3io alive (p3iodrv_ddr_get_version).
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param enable true to enable the watchdog, false to disable
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_set_watchdog(HANDLE handle, bool enable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the dip switches from the p3io.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param state Pointer to a uint8 to write the resulting dip switch state to
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_get_dipsw(HANDLE handle, uint8_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the detected cabinet type from the p3io.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param type Pointer to a enum p3io_cab_type to write the resulting state to
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_get_cab_type(HANDLE handle, enum p3io_cab_type *type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the detected video frequency of the monitor from the p3io.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param type Pointer to a enum p3io_video_freq to write the resulting state to
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_get_video_freq(HANDLE handle, enum p3io_video_freq *freq);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the JAMMA edge/input from the p3io.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param type Pointer to a struct p3io_ddr_jamma to write the resulting state
|
||||||
|
* to
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_ddr_get_jamma(HANDLE handle, struct p3io_ddr_jamma *jamma);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (some cabinet) light output state on the p3io.
|
||||||
|
*
|
||||||
|
* @param handle A handle to an opened p3io device.
|
||||||
|
* @param type Pointer to a struct p3io_ddr_output with the light output data to
|
||||||
|
* set on the p3io
|
||||||
|
* @result S_OK on success, any other HRESULT value on error
|
||||||
|
*/
|
||||||
|
HRESULT
|
||||||
|
p3iodrv_ddr_set_outputs(HANDLE handle, const struct p3io_ddr_output *state);
|
||||||
|
|
||||||
|
#endif
|
429
src/main/p3iodrv/device.c
Normal file
429
src/main/p3iodrv/device.c
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
#define LOG_MODULE "p3iodrv-device"
|
||||||
|
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
// Don't format because the order is important here
|
||||||
|
#include <windows.h>
|
||||||
|
#include <setupapi.h>
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
#include "p3io/cmd.h"
|
||||||
|
#include "p3io/guid.h"
|
||||||
|
#include "p3io/ioctl.h"
|
||||||
|
|
||||||
|
#include "p3iodrv/device.h"
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
#include "util/str.h"
|
||||||
|
|
||||||
|
#define P3IO_DEVICE_FILENMAME "\\p3io"
|
||||||
|
|
||||||
|
static HRESULT _p3iodrv_write_file_iobuf(HANDLE handle, struct iobuf *buffer)
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
DWORD bytes_processed;
|
||||||
|
|
||||||
|
if (!WriteFile(
|
||||||
|
handle, buffer->bytes, buffer->pos, &bytes_processed, NULL)) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("WriteFile failed: %lX", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("Written length: %ld", bytes_processed);
|
||||||
|
|
||||||
|
if (bytes_processed != buffer->pos) {
|
||||||
|
log_warning(
|
||||||
|
"WriteFile didn't finish: %ld != %Id",
|
||||||
|
bytes_processed,
|
||||||
|
buffer->pos);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_BAD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HRESULT _p3iodrv_read_file_iobuf(HANDLE handle, struct iobuf *buffer)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
DWORD bytes_processed;
|
||||||
|
|
||||||
|
if (!ReadFile(
|
||||||
|
handle,
|
||||||
|
buffer->bytes + buffer->pos,
|
||||||
|
buffer->nbytes - buffer->pos,
|
||||||
|
&bytes_processed,
|
||||||
|
NULL)) {
|
||||||
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("ReadFile failed: %lX", hr);
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("Read length: %ld", bytes_processed);
|
||||||
|
|
||||||
|
buffer->pos += bytes_processed;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HRESULT
|
||||||
|
_p3iodrv_write_message(HANDLE handle, const union p3io_req_any *req)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t req_framed_bytes[P3IO_MAX_MESSAGE_SIZE];
|
||||||
|
|
||||||
|
struct iobuf req_deframed;
|
||||||
|
struct iobuf req_framed;
|
||||||
|
|
||||||
|
memset(req_framed_bytes, 0, sizeof(req_framed_bytes));
|
||||||
|
|
||||||
|
// Used for logging the buffer, only
|
||||||
|
req_deframed.bytes = (uint8_t *) req;
|
||||||
|
req_deframed.nbytes = sizeof(union p3io_req_any);
|
||||||
|
req_deframed.pos = req->hdr.nbytes + 1;
|
||||||
|
|
||||||
|
req_framed.bytes = req_framed_bytes;
|
||||||
|
req_framed.nbytes = sizeof(req_framed_bytes);
|
||||||
|
req_framed.pos = 0;
|
||||||
|
|
||||||
|
iobuf_log(&req_deframed, "p3iodrv-device request deframed");
|
||||||
|
|
||||||
|
hr = p3io_frame_encode(&req_framed, req, req->hdr.nbytes + 1);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("Encoding request payload failed: %lX", hr);
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
iobuf_log(&req_framed, "p3iodrv-device request framed");
|
||||||
|
|
||||||
|
hr = _p3iodrv_write_file_iobuf(handle, &req_framed);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a full response message and not just a part of it.
|
||||||
|
*
|
||||||
|
* Because someone decided to implement a general "streaming bytes" solution
|
||||||
|
* with framing (ACIO) on top of a streaming byte solution with framing (USB),
|
||||||
|
* there is no guarantee that single reads are complete. This even applies to
|
||||||
|
* reads that are less then the max USB endpoint size.
|
||||||
|
*
|
||||||
|
* When polling to read data too fast in succession to writes, e.g. on a p3io
|
||||||
|
* command which first writes a request, then reads a response, the p3io
|
||||||
|
* hardware might not be fast enough to put all bytes of the full response into
|
||||||
|
* the device hardware side buffer. It appears that the device hardware/the
|
||||||
|
* firmware pushes out chunks of/single bytes instead of preparing full messages
|
||||||
|
* that are framed to the available USB buffer size.
|
||||||
|
*
|
||||||
|
* Therefore, messages can get fragmented and require multiple read calls to
|
||||||
|
* ensure the read buffer is fully flushed on the device side and received on
|
||||||
|
* the host. The following solution does exactly that it "reads until read
|
||||||
|
* everything". The following heuristics are applied to determine when we think
|
||||||
|
* a response message is fully received:
|
||||||
|
*
|
||||||
|
* 1. Read until you get parts of a header to determine how long the entire ACIO
|
||||||
|
* message is supposed to be. This must assume that the device will always put
|
||||||
|
* the correct amount of bytes onto the wire at some point. If it stopps doing
|
||||||
|
* that, there is no way to possibility to detect and take action on that.
|
||||||
|
* 2. Read until you get the complete ACIO paket based on the ACIO paket length
|
||||||
|
* field.
|
||||||
|
*/
|
||||||
|
static HRESULT
|
||||||
|
_p3iodrv_read_full_response_message(HANDLE handle, union p3io_resp_any *resp)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
uint8_t resp_framed_bytes[P3IO_MAX_MESSAGE_SIZE];
|
||||||
|
|
||||||
|
struct iobuf resp_framed;
|
||||||
|
struct iobuf resp_framed_flip_read;
|
||||||
|
struct iobuf resp_deframed;
|
||||||
|
|
||||||
|
memset(resp_framed_bytes, 0, sizeof(resp_framed_bytes));
|
||||||
|
|
||||||
|
resp_framed.bytes = resp_framed_bytes;
|
||||||
|
resp_framed.nbytes = sizeof(resp_framed_bytes);
|
||||||
|
resp_framed.pos = 0;
|
||||||
|
|
||||||
|
log_misc("Receiving response");
|
||||||
|
|
||||||
|
// Read as long as we do not consider the message to be complete
|
||||||
|
while (true) {
|
||||||
|
hr = _p3iodrv_read_file_iobuf(handle, &resp_framed);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read at least 0xAA + header size, otherwise keep reading
|
||||||
|
if (resp_framed.pos < 1 + sizeof(struct p3io_hdr)) {
|
||||||
|
log_misc(
|
||||||
|
"Read truncated message, length %ld less than header, read "
|
||||||
|
"again",
|
||||||
|
(DWORD) resp_framed.pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("Response received, length: %ld", (DWORD) resp_framed.pos);
|
||||||
|
|
||||||
|
iobuf_log(&resp_framed, "p3iodrv-device response framed");
|
||||||
|
|
||||||
|
// Potential pre-mature deframing because we cannot be sure we already
|
||||||
|
// received the entire frame
|
||||||
|
|
||||||
|
// Use a flipped buffer to read from the framed response
|
||||||
|
resp_framed_flip_read.bytes = resp_framed.bytes;
|
||||||
|
resp_framed_flip_read.nbytes = resp_framed.pos;
|
||||||
|
resp_framed_flip_read.pos = 0;
|
||||||
|
|
||||||
|
// Init for de-framing
|
||||||
|
resp_deframed.bytes = resp->raw.data;
|
||||||
|
resp_deframed.nbytes = sizeof(resp->raw);
|
||||||
|
resp_deframed.pos = 0;
|
||||||
|
|
||||||
|
hr = p3io_frame_decode(
|
||||||
|
&resp_deframed, (struct const_iobuf *) &resp_framed_flip_read);
|
||||||
|
|
||||||
|
iobuf_log(&resp_deframed, "p3iodrv-device response deframed");
|
||||||
|
|
||||||
|
// Verify if the de-framed message is actually complete
|
||||||
|
// +1 for the length byte
|
||||||
|
if (resp->hdr.nbytes + 1 > resp_deframed.pos) {
|
||||||
|
log_warning(
|
||||||
|
"Truncated de-framed message, length %ld less than size on "
|
||||||
|
"header %ld, read again",
|
||||||
|
(DWORD) resp_deframed.pos,
|
||||||
|
(DWORD) resp->hdr.nbytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eror check decoding only after verification that frame is completed
|
||||||
|
// Otherwise, this leads to failed decodings on truncated pakets that
|
||||||
|
// require another read
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("Decoding response payload failed: %lX", hr);
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"Recieved complete response, length de-framed %ld",
|
||||||
|
(DWORD) resp_deframed.pos);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_device_scan(char path[MAX_PATH])
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
PSP_DEVICE_INTERFACE_DETAIL_DATA_A detail_data;
|
||||||
|
HDEVINFO devinfo;
|
||||||
|
DWORD required_size;
|
||||||
|
SP_DEVICE_INTERFACE_DATA interface_data;
|
||||||
|
DWORD err;
|
||||||
|
|
||||||
|
detail_data = NULL;
|
||||||
|
|
||||||
|
devinfo = SetupDiGetClassDevsA(
|
||||||
|
&p3io_guid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
|
||||||
|
|
||||||
|
if (devinfo == INVALID_HANDLE_VALUE) {
|
||||||
|
// No device with GUID found
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||||
|
|
||||||
|
if (!SetupDiEnumDeviceInterfaces(
|
||||||
|
devinfo, NULL, &p3io_guid, 0, &interface_data)) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("SetupDiEnumDeviceInterfaces failed: %lX", res);
|
||||||
|
SetupDiDestroyDeviceInfoList(devinfo);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SetupDiGetDeviceInterfaceDetailA(
|
||||||
|
devinfo, &interface_data, NULL, 0, &required_size, NULL)) {
|
||||||
|
err = GetLastError();
|
||||||
|
res = HRESULT_FROM_WIN32(err);
|
||||||
|
|
||||||
|
if (err != ERROR_INSUFFICIENT_BUFFER) {
|
||||||
|
log_warning("SetupDiGetDeviceInterfaceDetailA failed: %lX", res);
|
||||||
|
SetupDiDestroyDeviceInfoList(devinfo);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detail_data = malloc(required_size);
|
||||||
|
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A);
|
||||||
|
|
||||||
|
if (!SetupDiGetDeviceInterfaceDetailA(
|
||||||
|
devinfo, &interface_data, detail_data, required_size, NULL, NULL)) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("SetupDiGetDeviceInterfaceDetailA failed: %lX", res);
|
||||||
|
free(detail_data);
|
||||||
|
SetupDiDestroyDeviceInfoList(devinfo);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
str_cpy(path, MAX_PATH, detail_data->DevicePath);
|
||||||
|
str_cat(path, MAX_PATH, P3IO_DEVICE_FILENMAME);
|
||||||
|
|
||||||
|
free(detail_data);
|
||||||
|
SetupDiDestroyDeviceInfoList(devinfo);
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_device_open(const char *path, HANDLE *handle)
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
|
||||||
|
log_assert(path);
|
||||||
|
log_assert(handle);
|
||||||
|
|
||||||
|
*handle = CreateFileA(
|
||||||
|
path,
|
||||||
|
GENERIC_READ | GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
NULL,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
0,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
if (*handle == INVALID_HANDLE_VALUE) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("CreateFileA failed: %lX", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_device_close(HANDLE *handle)
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
|
||||||
|
log_assert(handle);
|
||||||
|
log_assert(*handle != INVALID_HANDLE_VALUE);
|
||||||
|
|
||||||
|
if (CloseHandle(*handle) == FALSE) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("Closing failed: %lX", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
*handle = INVALID_HANDLE_VALUE;
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_device_read_version(
|
||||||
|
HANDLE handle, char version[P3IODRV_VERSION_MAX_LEN])
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
DWORD bytes_returned;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
|
||||||
|
memset(version, 0, sizeof(char[P3IODRV_VERSION_MAX_LEN]));
|
||||||
|
|
||||||
|
if (!DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
P3IO_IOCTL_GET_VERSION,
|
||||||
|
NULL,
|
||||||
|
0,
|
||||||
|
version,
|
||||||
|
sizeof(char[P3IODRV_VERSION_MAX_LEN]),
|
||||||
|
&bytes_returned,
|
||||||
|
NULL)) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("DeviceIoControl failed: %lX", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes_returned < 1) {
|
||||||
|
log_warning(
|
||||||
|
"DeviceIoControl returned size invalid: %ld", bytes_returned);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_BAD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT
|
||||||
|
p3iodrv_device_read_jamma(HANDLE handle, uint32_t jamma[P3IO_DRV_JAMMA_MAX_LEN])
|
||||||
|
{
|
||||||
|
HRESULT res;
|
||||||
|
DWORD bytes_returned;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
|
||||||
|
if (!DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
P3IO_IOCTL_READ_JAMMA,
|
||||||
|
NULL,
|
||||||
|
0,
|
||||||
|
jamma,
|
||||||
|
sizeof(uint32_t[P3IO_DRV_JAMMA_MAX_LEN]),
|
||||||
|
&bytes_returned,
|
||||||
|
NULL)) {
|
||||||
|
res = HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
log_warning("ioctl read jamma failed: %lX", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes_returned != sizeof(uint32_t[P3IO_DRV_JAMMA_MAX_LEN])) {
|
||||||
|
log_warning(
|
||||||
|
"DeviceIoControl returned size invalid: %ld", bytes_returned);
|
||||||
|
return HRESULT_FROM_WIN32(ERROR_BAD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT p3iodrv_device_transfer(
|
||||||
|
HANDLE handle, const union p3io_req_any *req, union p3io_resp_any *resp)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
|
||||||
|
log_assert(handle != INVALID_HANDLE_VALUE);
|
||||||
|
log_assert(req);
|
||||||
|
log_assert(resp);
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"TRANSFER START, req nbytes %d, seq_no %d, cmd 0x%X",
|
||||||
|
req->hdr.nbytes,
|
||||||
|
req->hdr.seq_no,
|
||||||
|
req->hdr.cmd);
|
||||||
|
|
||||||
|
hr = _p3iodrv_write_message(handle, req);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = _p3iodrv_read_full_response_message(handle, resp);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"TRANSFER FINISHED, resp nbytes %d, seq_no %d, cmd 0x%X",
|
||||||
|
resp->hdr.nbytes,
|
||||||
|
resp->hdr.seq_no,
|
||||||
|
resp->hdr.cmd);
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
105
src/main/p3iodrv/device.h
Normal file
105
src/main/p3iodrv/device.h
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#ifndef P3IODRV_DEVICE_H
|
||||||
|
#define P3IODRV_DEVICE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "p3io/cmd.h"
|
||||||
|
|
||||||
|
#include "util/iobuf.h"
|
||||||
|
|
||||||
|
#define P3IODRV_VERSION_MAX_LEN 128
|
||||||
|
#define P3IO_DRV_JAMMA_MAX_LEN 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan for a connected p3io device. Currently, this does not support multiple
|
||||||
|
* p3io devices and only returns the "first one" found.
|
||||||
|
*
|
||||||
|
* @param path Buffer of size MAX_PATH this function writes the full device path
|
||||||
|
* to when a device was found
|
||||||
|
* @return S_OK when a device was found, ERROR_FILE_NOT_FOUND if none was found
|
||||||
|
* or any other HRESULT error on any errors while scanning occured.
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_scan(char path[MAX_PATH]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a p3io device by the given device path.
|
||||||
|
*
|
||||||
|
* @param path Pointer to a buffer with the full device path of the p3io device
|
||||||
|
* to open
|
||||||
|
* @param handle Pointer to a handle variable to return the opened device handle
|
||||||
|
* to when opening was successful
|
||||||
|
* @return S_OK if opening was successful, ERROR_FILE_NOT_FOUND if the device
|
||||||
|
* with the given path was not found, or any other HRESULT error when
|
||||||
|
* trying to open the device.
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_open(const char *path, HANDLE *handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close an open p3io device by its handle
|
||||||
|
*
|
||||||
|
* @param handle Pointer to the handle to close. The variable will be set to
|
||||||
|
* INVALID_HANDLE_VALUE on success
|
||||||
|
* @return S_OK if closing was successful, or any other HRESULT value on errors
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_close(HANDLE *handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the version of the p3io device.
|
||||||
|
*
|
||||||
|
* This call is not guaranteed to be supported with all p3io drivers, e.g. we
|
||||||
|
* know this is supported by the 64-bit p3io driver but apparently not by the
|
||||||
|
* 32-bit one.
|
||||||
|
*
|
||||||
|
* @param handle A valid handle to an opened p3io device
|
||||||
|
* @param version Pointer to a buffer to write the resulting version string to
|
||||||
|
* @return S_OK if the operation was successful, or any other HRESULT value on
|
||||||
|
* errors
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_read_version(
|
||||||
|
HANDLE handle, char version[P3IODRV_VERSION_MAX_LEN]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read input data from the JAMMA edge. As this is supported by different games
|
||||||
|
* and a core functionality of the p3io, the data is not further specified
|
||||||
|
* here. This function is optimised for low latency inputs by continously
|
||||||
|
* polling it on the host.
|
||||||
|
*
|
||||||
|
* @param handle A valid handle to an opened p3io device
|
||||||
|
* @param jamma Buffer to write the read input data to on success
|
||||||
|
* @return S_OK if the operation was successful, or any other HRESULT value on
|
||||||
|
* errors
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_read_jamma(
|
||||||
|
HANDLE handle, uint32_t jamma[P3IO_DRV_JAMMA_MAX_LEN]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a generic data transfer.
|
||||||
|
*
|
||||||
|
* This is _the_ generic messaging interface of the p3io to exchange data with
|
||||||
|
* the device. Depending on the type of p3io and firmware, different command
|
||||||
|
* codes support different functionality.
|
||||||
|
*
|
||||||
|
* Note that this interface is not optimized for low latency data transfers
|
||||||
|
* and is a fully blocking "endpoint". The caller of this function is blocked
|
||||||
|
* entirely until the host request is fully processed and the device response is
|
||||||
|
* fully received.
|
||||||
|
*
|
||||||
|
* Implementation detail: It implements (sort of?) the ACIO protocol on top of
|
||||||
|
* the USB as a transport layer. Encoding and framing is being handling by this
|
||||||
|
* function transparently, so the caller does not have to worry about that
|
||||||
|
* anymore.
|
||||||
|
*
|
||||||
|
* @param handle A valid handle to an opened p3io device
|
||||||
|
* @param req Pointer to a buffer/struct with a p3io formatted request
|
||||||
|
* @param resp Pointer to a buffer to receive the device's response to. Ensure
|
||||||
|
* this is large enough for the expceted response matching the
|
||||||
|
* issued type of request.
|
||||||
|
* @return S_OK if the transfer was successful and a response was received and
|
||||||
|
* written to the buffer, or any other HRESULT value on errors
|
||||||
|
*/
|
||||||
|
HRESULT p3iodrv_device_transfer(
|
||||||
|
HANDLE handle, const union p3io_req_any *req, union p3io_resp_any *resp);
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in New Issue
Block a user