winraw: keyboard and mouse read in bulk by the poll, behind a switch

With RETROARCH_RAWINPUT_POLL=1 in the environment the raw input driver
makes its keyboard and mouse window on the thread that polls, and
reads the reports waiting for it in bulk - one GetRawInputBuffer() for
all of them - at the start of every poll and before that thread pumps
its messages. Without the variable the driver behaves as before.

As it is otherwise, the window is made on whichever thread runs the
video driver's init, and each report is a WM_INPUT message taken when
that thread pumps: a PeekMessage() and a GetRawInputData() per report,
once per video frame, and not while that thread waits in a present.

Latency: the poll, on the main thread, reads what the last pump left.
A key or a mouse report that arrived since is in the queue, not in the
state, and the core gets it a frame later. Read by the poll itself,
the state is what the devices had sent at the moment of the poll -
with late polling, the moment the core asks.

Cost: two system calls a report - at 60 fps some 33 a frame for a
1000 Hz mouse and some 270 for an 8000 Hz one. A bulk read is one call
for all of them and there is no message to dispatch. No thread is
added and nothing waits or wakes; the reads are made where the
frontend already is. (Counted from the code, not measured: the driver
logs reports, bulk reads and messages when it is freed.)

And the window is no longer the video driver's thread's, which has to
be true before the input driver can stop being restarted with the
video driver. That is a later step.

What else differs when this is on:

- input_keyboard_event() is called at the end of the poll, on the main
  thread, for the keys of the reports read. Otherwise the callback
  calls it from the window's thread - under threaded video the video
  thread - into the menu's state and the core's keyboard callback.
  The key table is written when the report is read.

- Which modifier keys are held is read from the driver's key table.
  The published value (win32_get_keyboard_mods()) comes from the main
  window's thread and can be a frame behind a report just read; only
  the lock keys' toggles are taken from it.

- A bulk read takes every raw input report waiting on the thread. If
  winraw_joypad's window is on it too (video not threaded) its reports
  come out here and are passed on, through winraw_joypad_take_hid().

The driver's notes record an earlier attempt at bulk reads, d087a820cd,
reverted because keys and mouse buttons stopped working while mouse
movement did not, for a reason not found. It read from inside the
WM_INPUT handler. The report a WM_INPUT message carries has already
left the queue when its handler runs, so a bulk read there never sees
it - and a key press is usually the only report waiting, while a
moving mouse always has more behind it. Nothing here reads from inside
a handler. A report that does arrive as a message, because the pump
got to it first, is taken by the callback as before.

The callback's handling of a report moved into a function the bulk
read shares; with the switch off it does what it did.

RETROARCH_RAWINPUT_POLL=2 also registers as a sink, for the case that
=1 gets no input under threaded video. Raw input goes to a window only
while its application is in the foreground; if Windows judges that by
process, a window on a thread that does not have the focus gets it. If
it judges by thread, nothing arrives; as a sink everything does and
the main window's focus decides what is taken. Wine delivers with =1.

A 32-bit build on 64-bit Windows gets its bulk records laid out for 64
bits; that is allowed for. Compiled with the 32-bit compiler, not run.

samples/input/winraw_poll is the real driver under Wine, with the
focused window made and pumped by a second thread and the driver
started from it, as under threaded video: the switch off leaves the
driver as it was; on, the first poll makes the window on the polling
thread; six keys injected while nothing pumps are read by one bulk
read, none as a message, none left in the queue, and come out of the
poll as six events in order with shift on the right ones; a report
the pump dispatches first is still taken; the pump's hook reads on
the window's thread and not on another; free and twenty restarts. The
two existing winraw tests pass. In CI.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-03 06:21:19 +02:00
1 parent e62d8258cb
commit de2cc6ddb7
7 files changed
+1012 -37

No files matched your search

+26
View File
@@ -493,3 +493,29 @@ jobs:
WINEDEBUG=-all timeout 300 make check
make clean
echo "[pass] winraw_abs_pos_test (wine)"
- name: Build and run winraw_poll_test (mingw + Wine)
shell: bash
working-directory: samples/input/winraw_poll
run: |
set -eu
# input/drivers/winraw_input.c with its keyboard and mouse
# read in bulk by the poll (RETROARCH_RAWINPUT_POLL). The real
# driver under Wine on a virtual display, with the focused
# window made and pumped by a second thread as the video
# thread does, and the driver started from that thread. With
# the switch off the driver is as it was. With it on: init
# makes no window and the first poll does, on the polling
# thread; six keys injected with SendInput() while nothing
# pumps are read by one bulk read, none as a message, none
# left in the queue, and come out of the poll as six key
# events in order with the modifier held at the time; a
# report the pump dispatches first is still taken; the
# pump's hook reads on the window's thread and not on
# another; free takes the window down, twenty restarts each
# do the same.
trap 'wineserver -k >/dev/null 2>&1 || true' EXIT
make clean all
WINEDEBUG=-all timeout 300 make check
make clean
echo "[pass] winraw_poll_test (wine)"
+441 -37
View File
@@ -48,6 +48,10 @@ extern "C" {
/* Threading model
* ---------------
* (As it is by default. With RETROARCH_RAWINPUT_POLL the window is
* the polling thread's and the reports are read in bulk by the poll:
* see "Read by the poll" further down.)
*
* On every Windows video driver that can actually use this input
* driver, it is built by the video context driver's input_driver
* callback - gfx_ctx_wgl_input_driver() and the w_vk / d3d_common /
@@ -173,6 +177,13 @@ struct winraw_pointer_status
int pointer_y;
};
/* Key events held until the end of a poll. A frame's worth is a
* handful. */
#define WINRAW_KEV_SIZE 256
/* One bulk read's worth of reports: over a thousand mouse reports. */
#define WINRAW_DRAIN_BYTES (64 * 1024)
enum winraw_input_flags
{
WRAW_INP_FLG_MOUSE_GRAB = (1 << 0),
@@ -201,6 +212,24 @@ typedef struct
uint8_t flags;
bool last_focus;
bool kb_clear_pending;
/* Read in bulk by the poll: see "Read by the poll" below. All of
* this is unused, and zero, unless that is switched on. */
bool poll_drain;
/* Registered as a sink: input arrives whether or not the
* application is in the foreground, and the main window's focus
* decides what is taken. */
bool sink;
bool window_failed;
/* Key events of the reports read so far, handed on at the end of
* the poll. One thread's. */
unsigned kev_n;
uint32_t kev[WINRAW_KEV_SIZE];
/* Counts, logged when the driver is freed. */
unsigned long drained; /* reports read in bulk */
unsigned long drain_reads; /* in this many reads */
unsigned long by_message; /* reports that came as a message */
unsigned long kev_dropped;
} winraw_input_t;
/* TODO/FIXME - static globals */
@@ -684,31 +713,158 @@ static void winraw_update_mouse_state(winraw_input_t *wr,
}
}
static LRESULT CALLBACK winraw_callback(
HWND wnd, UINT msg, WPARAM wpar, LPARAM lpar)
/* Read by the poll
* ----------------
* With RETROARCH_RAWINPUT_POLL set in the environment, the keyboard
* and mouse window is made by the first winraw_poll(), on the thread
* that polls, and the reports waiting for it are read in bulk - one
* GetRawInputBuffer() for all of them - at the start of every poll
* and before that thread pumps its messages.
*
* As it is otherwise (the threading note at the top), the window is
* made on whichever thread runs the video driver's init, and each
* report is a WM_INPUT message taken when that thread pumps: a
* PeekMessage() and a GetRawInputData() per report, once per video
* frame, and not at all while that thread waits in a present. Two
* things follow.
*
* Latency. The poll, on the main thread, reads what the last pump
* left. A key or a mouse report that arrived since is in the queue,
* not in the state, and the core gets it a frame later. Read by the
* poll itself, the state is whatever the devices had sent at the
* moment of the poll - with late polling, the moment the core asks.
*
* Cost. Two system calls a report: at 60 fps a 1000 Hz mouse is some
* 33 a frame and an 8000 Hz one some 270. A bulk read is one call for
* all of them, and there is no message to dispatch. No thread is
* added for this and nothing waits or wakes: the reads happen where
* the frontend already is.
*
* And the window is no longer the video driver's thread's, which has
* to be true before the input driver can stop being restarted with
* the video driver. (It still is restarted with it: a later step.)
*
* What else differs when this is on:
*
* - Key events. A report's key is written to the key table when the
* report is read. The call to input_keyboard_event() - into the
* menu's state and the core's keyboard callback - is held until the
* end of the poll, and so is made on the main thread; as it is
* otherwise, under threaded video it is made from the video thread.
*
* - Modifiers. win32_get_keyboard_mods() is what the main window's
* thread last published from its own key messages, which can be a
* frame behind a report just read. Which modifier keys are held is
* read from the key table instead; only the lock keys' toggles are
* taken from the published value.
*
* - A bulk read takes every raw input report waiting on the thread,
* whichever window it was for. If winraw_joypad's window is on this
* thread too (video not threaded), its reports come out here and
* are passed to it (winraw_joypad_take_hid()). An attempt at bulk
* reads before this one, d087a820cd, read from inside the WM_INPUT
* handler and lost keys and buttons; the report a WM_INPUT message
* carries has already left the queue by the time its handler runs,
* so a bulk read there never sees it, and a key press is usually
* the only report waiting. Nothing here reads from inside a handler.
* A report that does arrive as a message - the pump got to it first
* - is taken by the callback as before.
*
* RETROARCH_RAWINPUT_POLL=2 also registers as a sink. Raw input goes
* to a window only while its application is in the foreground, and
* the expectation is that Windows judges that by process, so that a
* window on a thread that does not have the focus still gets it. If
* it judges by thread, nothing would arrive with =1 under threaded
* video; as a sink everything arrives, and the main window's focus
* (winraw_focus) decides whether it is taken. */
extern void winraw_joypad_take_hid(HANDLE device, const BYTE *data, DWORD size);
/* The thread whose queue is read in bulk, and its driver. Checked by
* thread id first: the driver is that thread's to free. */
static DWORD winraw_drain_tid;
static winraw_input_t *winraw_drain_wr;
/* 8 when this is a 32-bit process on 64-bit Windows: a bulk read's
* records are then laid out for 64 bits, the payload eight bytes
* further on and the records aligned to eight. (GetRawInputData()
* corrects its one record itself.) */
static unsigned winraw_wow64_shift(void)
{
static uint8_t data[1024];
RAWINPUT *ri = (RAWINPUT*)data;
UINT size = sizeof(data);
if (msg != WM_INPUT)
return DefWindowProcA(wnd, msg, wpar, lpar);
if (!(
GET_RAWINPUT_CODE_WPARAM(wpar) != RIM_INPUT /* app is in the background */
|| GetRawInputData((HRAWINPUT)lpar, RID_INPUT,
data, &size, sizeof(RAWINPUTHEADER)) == (UINT)-1))
#ifdef _WIN64
return 0;
#else
static int shift = -1;
if (shift < 0)
{
unsigned i;
unsigned mcode, flags, down, mod;
winraw_input_t *wr = (winraw_input_t*)(LONG_PTR)
GetWindowLongPtr(wnd, GWLP_USERDATA);
typedef BOOL (WINAPI *is_wow64_t)(HANDLE, PBOOL);
BOOL wow = FALSE;
is_wow64_t is_wow = (is_wow64_t)GetProcAddress(
GetModuleHandleA("kernel32"), "IsWow64Process");
if (is_wow && is_wow(GetCurrentProcess(), &wow) && wow)
shift = 8;
else
shift = 0;
}
return (unsigned)shift;
#endif
}
switch (ri->header.dwType)
{
case RIM_TYPEKEYBOARD:
mcode = ri->data.keyboard.MakeCode;
flags = ri->data.keyboard.Flags;
/* scancode in the low 16 bits, down in bit 16, modifiers above */
#define WINRAW_KEV_PACK(mcode, down, mod) \
((uint32_t)(mcode) | ((uint32_t)((down) ? 1 : 0) << 16) | ((uint32_t)(mod) << 17))
static uint16_t winraw_held_mods(const winraw_input_t *wr)
{
uint16_t mod = win32_get_keyboard_mods()
& (RETROKMOD_CAPSLOCK | RETROKMOD_NUMLOCK | RETROKMOD_SCROLLOCK);
if (wr->kb_keys[SC_LSHIFT] || wr->kb_keys[SC_RSHIFT])
mod |= RETROKMOD_SHIFT;
if (wr->kb_keys[SC_LCTRL] || wr->kb_keys[SC_RCTRL])
mod |= RETROKMOD_CTRL;
if (wr->kb_keys[SC_LALT] || wr->kb_keys[SC_RALT])
mod |= RETROKMOD_ALT;
if (wr->kb_keys[SC_LSUPER] || wr->kb_keys[SC_RSUPER])
mod |= RETROKMOD_META;
return mod;
}
/* Hands the held key events on. Called last in winraw_poll(), from a
* copy, and touches the driver no more once the first is delivered:
* what an event sets off is not this function's to know. */
static void winraw_kev_deliver(winraw_input_t *wr)
{
uint32_t ev[WINRAW_KEV_SIZE];
unsigned i;
unsigned n = wr->kev_n;
if (!n)
return;
memcpy(ev, wr->kev, n * sizeof(ev[0]));
wr->kev_n = 0;
for (i = 0; i < n; i++)
input_keyboard_event((ev[i] >> 16) & 1,
input_keymaps_translate_keysym_to_rk(ev[i] & 0xFFFF),
0, (uint16_t)(ev[i] >> 17), RETRO_DEVICE_KEYBOARD);
}
/* One keyboard or mouse report, however it was read. False for a
* scancode that is ignored. */
static bool winraw_take(winraw_input_t *wr, DWORD type, HANDLE device,
void *payload)
{
unsigned i;
unsigned mcode, flags, down, mod;
switch (type)
{
case RIM_TYPEKEYBOARD:
{
RAWKEYBOARD *kb = (RAWKEYBOARD*)payload;
mcode = kb->MakeCode;
flags = kb->Flags;
down = (flags & RI_KEY_BREAK) ? 0 : 1;
mod = 0;
@@ -738,34 +894,194 @@ static LRESULT CALLBACK winraw_callback(
case 0xE036:
case 0xE0AA:
case 0xE0B6:
return 0;
return false;
}
mod = win32_get_keyboard_mods();
wr->kb_keys[mcode] = down;
input_keyboard_event(down,
input_keymaps_translate_keysym_to_rk(mcode),
0, mod, RETRO_DEVICE_KEYBOARD);
break;
case RIM_TYPEMOUSE:
for (i = 0; i < wr->mouse_cnt; ++i)
if (wr->poll_drain)
{
if (g_mice[i].hnd == ri->header.hDevice)
{
winraw_update_mouse_state(wr,
&g_mice[i], &ri->data.mouse);
break;
}
/* held until the end of the poll */
if (wr->kev_n < WINRAW_KEV_SIZE)
wr->kev[wr->kev_n++] =
WINRAW_KEV_PACK(mcode, down, winraw_held_mods(wr));
else
wr->kev_dropped++;
}
break;
else
input_keyboard_event(down,
input_keymaps_translate_keysym_to_rk(mcode),
0, mod, RETRO_DEVICE_KEYBOARD);
}
break;
case RIM_TYPEMOUSE:
for (i = 0; i < wr->mouse_cnt; ++i)
{
if (g_mice[i].hnd == device)
{
winraw_update_mouse_state(wr,
&g_mice[i], (RAWMOUSE*)payload);
break;
}
}
break;
}
return true;
}
/* Everything waiting in this thread's raw input queue, in as few
* reads as it takes: one, unless there is more than a buffer's worth.
* Only for the thread the window is on. */
static void winraw_drain(winraw_input_t *wr)
{
static uint64_t buf[WINRAW_DRAIN_BYTES / sizeof(uint64_t)];
const unsigned shift = winraw_wow64_shift();
const size_t align = shift ? 8 : sizeof(void*);
for (;;)
{
UINT i;
UINT size = sizeof(buf);
BYTE *p = (BYTE*)buf;
UINT n = GetRawInputBuffer((PRAWINPUT)buf, &size,
sizeof(RAWINPUTHEADER));
/* nothing waiting; or a report bigger than the buffer, which is
* left to arrive as a message */
if (n == 0 || n == (UINT)-1)
break;
wr->drain_reads++;
for (i = 0; i < n; i++)
{
RAWINPUT *ri = (RAWINPUT*)p;
BYTE *payload = (BYTE*)&ri->data + shift;
wr->drained++;
if (ri->header.dwType == RIM_TYPEHID)
{
/* a controller's, for winraw_joypad's window on this
* thread */
RAWHID *hid = (RAWHID*)payload;
winraw_joypad_take_hid(ri->header.hDevice, hid->bRawData,
hid->dwSizeHid * hid->dwCount);
}
/* as a sink, background input arrives as well */
else if (!wr->sink || winraw_focus)
winraw_take(wr, ri->header.dwType, ri->header.hDevice, payload);
p += (ri->header.dwSize + align - 1) & ~(align - 1);
}
/* It had room for as much again: the queue is empty, and the
* read that would say so is saved. */
if ((size_t)(p - (BYTE*)buf) < sizeof(buf) / 2)
break;
}
}
/* For the thread's pump (ui_application_win32_process_events()), so
* that reports are read in bulk before it rather than dispatched by
* it one message at a time. Nothing unless this is the thread the
* window is on. */
void winraw_drain_queue(void)
{
if (winraw_drain_tid && GetCurrentThreadId() == winraw_drain_tid)
winraw_drain(winraw_drain_wr);
}
/* Input that came while the application was in the background is not
* taken - unless the driver registered as a sink, where everything
* that is not foreground input comes marked that way and the main
* window's focus decides. */
static bool winraw_in_background(const winraw_input_t *wr, WPARAM wpar)
{
if (GET_RAWINPUT_CODE_WPARAM(wpar) == RIM_INPUT)
return false;
return !(wr->sink && winraw_focus);
}
static LRESULT CALLBACK winraw_callback(
HWND wnd, UINT msg, WPARAM wpar, LPARAM lpar)
{
static uint8_t data[1024];
RAWINPUT *ri = (RAWINPUT*)data;
UINT size = sizeof(data);
winraw_input_t *wr;
if (msg != WM_INPUT)
return DefWindowProcA(wnd, msg, wpar, lpar);
/* none once the driver has let go of the window */
if (!(wr = (winraw_input_t*)(LONG_PTR)
GetWindowLongPtr(wnd, GWLP_USERDATA)))
return DefWindowProcA(wnd, msg, wpar, lpar);
if (!(
winraw_in_background(wr, wpar)
|| GetRawInputData((HRAWINPUT)lpar, RID_INPUT,
data, &size, sizeof(RAWINPUTHEADER)) == (UINT)-1))
{
wr->by_message++;
if (!winraw_take(wr, ri->header.dwType, ri->header.hDevice,
&ri->data))
return 0;
}
DefWindowProcA(wnd, msg, wpar, lpar);
return 0;
}
/* The window for the bulk reads: made by the first poll, so that it
* is the polling thread's, and registered for the keyboard and the
* mouse. */
static bool winraw_poll_window_up(winraw_input_t *wr)
{
RAWINPUTDEVICE rid[2];
DWORD base = wr->sink ? RIDEV_INPUTSINK : 0;
if (!(wr->window = winraw_create_window(winraw_callback)))
return false;
SetWindowLongPtr(wr->window, GWLP_USERDATA, (LONG_PTR)wr);
rid[0].dwFlags = base;
rid[0].hwndTarget = wr->window;
rid[0].usUsagePage = 0x01; /* Generic desktop */
rid[0].usUsage = 0x06; /* Keyboard */
if (config_get_ptr()->bools.input_nowinkey_enable)
rid[0].dwFlags |= RIDEV_NOHOTKEYS; /* Disable win keys while focused */
rid[1].dwFlags = base;
rid[1].hwndTarget = wr->window;
rid[1].usUsagePage = 0x01; /* generic desktop */
rid[1].usUsage = 0x02; /* mouse */
if ( RegisterRawInputDevices(&rid[0], 1, sizeof(RAWINPUTDEVICE))
&& RegisterRawInputDevices(&rid[1], 1, sizeof(RAWINPUTDEVICE)))
{
winraw_drain_wr = wr;
winraw_drain_tid = GetCurrentThreadId();
RARCH_LOG("[WinRaw] Keyboard and mouse are read in bulk by the poll"
" (thread %lu)%s.\n", (unsigned long)winraw_drain_tid,
wr->sink ? ", registered as a sink" : "");
return true;
}
SetWindowLongPtr(wr->window, GWLP_USERDATA, 0);
DestroyWindow(wr->window);
wr->window = NULL;
return false;
}
/* RETROARCH_RAWINPUT_POLL: 1, or 2 to register as a sink as well. */
static bool winraw_poll_wanted(bool *sink)
{
const char *env = getenv("RETROARCH_RAWINPUT_POLL");
*sink = (env && env[0] == '2');
return env && (env[0] == '1' || env[0] == '2');
}
static void *winraw_init(const char *joypad_driver)
{
RAWINPUTDEVICE rid;
@@ -778,6 +1094,23 @@ static void *winraw_init(const char *joypad_driver)
input_keymaps_init_keyboard_lut(rarch_key_map_winraw);
/* Read in bulk by the poll, if asked for: the window is then made
* by the first poll, on the thread that polls, not here. */
if (winraw_poll_wanted(&wr->sink))
{
wr->poll_drain = true;
if (!winraw_init_devices(&g_mice, &wr->mouse_cnt))
goto error;
if (wr->mouse_cnt)
{
if (!(wr->mice = (winraw_mouse_t*)
malloc(wr->mouse_cnt * sizeof(winraw_mouse_t))))
goto error;
memcpy(wr->mice, g_mice, wr->mouse_cnt * sizeof(winraw_mouse_t));
}
return wr;
}
if (!(wr->window = winraw_create_window(winraw_callback)))
goto error;
if (!winraw_init_devices(&g_mice, &wr->mouse_cnt))
@@ -872,6 +1205,20 @@ static void winraw_poll(void *data)
bool crs_pos_valid = false;
winraw_input_t *wr = (winraw_input_t*)data;
/* Everything the devices have sent up to now, before any of it is
* looked at below. */
if (wr->poll_drain)
{
if ( !wr->window && !wr->window_failed
&& !winraw_poll_window_up(wr))
{
wr->window_failed = true;
RARCH_ERR("[WinRaw] Could not make the keyboard and mouse window.\n");
}
if (wr->window)
winraw_drain(wr);
}
/* Fix coordinates after a resolution change. Runs here rather than
* in the wndproc so that active_rect, view_abs_ratio_* and the
* position all belong to this thread. */
@@ -1000,6 +1347,9 @@ static void winraw_poll(void *data)
wr->mice[i].whl_d = retro_atomic_exchange_int(&g_mice[i].whl_d, 0);
wr->mice[i].flags = g_mice[i].flags;
}
if (wr->poll_drain)
winraw_kev_deliver(wr);
}
static int16_t winraw_input_state(
@@ -1353,6 +1703,45 @@ static void winraw_free(void *data)
winraw_input_t *wr = (winraw_input_t*)data;
bool input_nowinkey_enable = config_get_ptr()->bools.input_nowinkey_enable;
if (wr->poll_drain)
{
if (wr->window)
{
rid.dwFlags = RIDEV_REMOVE;
rid.hwndTarget = NULL;
rid.usUsagePage = 0x01; /* generic desktop */
rid.usUsage = 0x02; /* mouse */
RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE));
rid.usUsage = 0x06; /* Keyboard */
RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE));
SetWindowLongPtr(wr->window, GWLP_USERDATA, 0);
/* A window is destroyed by the thread that made it. That is
* the polling thread, which is also the one that frees the
* driver; if it ever is not, the window is asked to close
* and goes when its own thread next pumps. */
if (GetCurrentThreadId() == winraw_drain_tid)
{
DestroyWindow(wr->window);
UnregisterClassA("winraw-input", NULL);
}
else
PostMessageA(wr->window, WM_CLOSE, 0, 0);
}
if (winraw_drain_wr == wr)
{
winraw_drain_tid = 0;
winraw_drain_wr = NULL;
}
RARCH_LOG("[WinRaw] Read by the poll: %lu reports in %lu bulk reads,"
" %lu more as messages (%lu key events dropped).\n",
wr->drained, wr->drain_reads, wr->by_message, wr->kev_dropped);
free(g_mice);
free(wr->mice);
free(data);
return;
}
rid.dwFlags = RIDEV_REMOVE;
rid.hwndTarget = NULL;
rid.usUsagePage = 0x01; /* generic desktop */
@@ -1400,7 +1789,22 @@ static void winraw_grab_mouse(void *d, bool state)
if (curr_state == state)
return;
rid.dwFlags = (wr->window) ? 0 : RIDEV_REMOVE;
/* read by the poll, and its first poll has not made the window
* yet: there is nothing to register again */
if (wr->poll_drain && !wr->window)
{
if (state)
wr->flags |= WRAW_INP_FLG_MOUSE_GRAB;
else
wr->flags &= ~WRAW_INP_FLG_MOUSE_GRAB;
#ifndef _XBOX
win32_clip_window(state);
#endif
return;
}
rid.dwFlags = (wr->window)
? (wr->sink ? RIDEV_INPUTSINK : 0) : RIDEV_REMOVE;
rid.hwndTarget = wr->window;
rid.usUsagePage = 0x01; /* generic desktop */
rid.usUsage = 0x02; /* mouse */
+13
View File
@@ -696,6 +696,19 @@ static void winraw_joypad_parse_hid_report(winraw_joypad_joypad_data_t *pad,
}
}
/* A report for one of this driver's controllers that was read in bulk
* by the keyboard and mouse driver (winraw_input.c, "Read by the
* poll"): a bulk read takes every raw input report waiting on its
* thread, and when this driver's window is on that thread, its
* reports with them. Same as the WM_INPUT case below from here on. */
void winraw_joypad_take_hid(HANDLE device, const BYTE *data, DWORD size)
{
int slot = winraw_joypad_find_pad(device);
if (slot >= 0)
winraw_joypad_parse_hid_report(&winraw_joypad_pads[slot],
data, size);
}
/* ------------------------------------------------------------------ */
/* Message-only window for receiving WM_INPUT / WM_INPUT_DEVICE_CHANGE */
/* ------------------------------------------------------------------ */
@@ -80,6 +80,8 @@ bool video_driver_translate_coord_viewport(struct video_viewport *vp,
uintptr_t video_driver_window_get(void) { return 0; }
void win32_clip_window(bool grab) { (void)grab; }
uint16_t win32_get_keyboard_mods(void) { return 0; }
void winraw_joypad_take_hid(HANDLE device, const BYTE *data, DWORD size)
{ (void)device; (void)data; (void)size; }
void win32_hotplug_arm(void) { }
bool win32_hotplug_due(void) { return false; }
+37
View File
@@ -0,0 +1,37 @@
TARGET := winraw_poll_test.exe
# The real winraw driver, cross-built with mingw-w64 and run under
# Wine. winraw_input.c is included by the test itself, for its statics;
# the frontend symbols it links against are stubbed in the test. With
# a display (xvfb-run) the test also injects keys and follows them
# through a bulk read; without one that part is skipped.
# make && make check
REPO_ROOT := ../../..
CC := x86_64-w64-mingw32-gcc
CFLAGS += -Wall -std=gnu89 -Wdeclaration-after-statement -g -O1 \
-DRARCH_INTERNAL -DHAVE_MENU -DHAVE_THREADS -DHAVE_WINRAWINPUT \
-I$(REPO_ROOT) -I$(REPO_ROOT)/input/include \
-I$(REPO_ROOT)/libretro-common/include -I$(REPO_ROOT)/deps
LDLIBS += -lhid -lsetupapi -luser32 -lgdi32
all: $(TARGET)
$(TARGET): winraw_poll_test.c $(REPO_ROOT)/input/drivers/winraw_input.c
$(CC) $(CFLAGS) -o $@ winraw_poll_test.c \
$(REPO_ROOT)/libretro-common/compat/compat_strl.c $(LDLIBS)
# wine goes by several names; the first one found is used
WINE ?= $(shell for w in wine wine64 /usr/lib/wine/wine64; do \
command -v $$w >/dev/null 2>&1 && { echo $$w; break; }; done)
XVFB ?= $(shell command -v xvfb-run >/dev/null 2>&1 && echo "xvfb-run -a")
check: all
@test -n "$(WINE)" || { echo "no wine" >&2; exit 1; }
WINEDEBUG=-all $(XVFB) $(WINE) ./$(TARGET)
clean:
rm -f $(TARGET)
.PHONY: all check clean
@@ -0,0 +1,480 @@
/* winraw: the keyboard and mouse read in bulk by the poll
* (RETROARCH_RAWINPUT_POLL).
*
* The real driver, cross-built with mingw-w64 and run under Wine on a
* virtual display. The window that has the focus is made and pumped
* by a second thread, as the RetroArch window is by the video thread
* under threaded video; the driver is started from that thread too,
* as a video driver's init starts it; the test's main thread polls.
*
* Checked here:
*
* - with the switch off the driver is as it was: its window is made by
* init, on init's thread, and nothing is read in bulk;
* - with it on, init makes no window; the first poll does, on the
* polling thread;
* - keys injected with SendInput() while nothing pumps are all there
* after one poll: in the key table, and as key events delivered on
* the polling thread, in order, with the modifier held at the time;
* they were read in bulk, none as a message, and none is left in the
* queue afterwards to be read twice;
* - a report the pump gets to first is still taken, through the
* callback, and its key event still waits for the poll;
* - the pump's hook reads in bulk on the window's thread and does
* nothing on another;
* - a controller's report in a bulk read is passed to the controller
* driver;
* - which modifier keys are held is read from the key table, and only
* the lock keys' toggles from the published value;
* - background input is refused, unless registered as a sink with the
* main window focused;
* - free takes the window down, and twenty restarts each do the same.
*
* What it cannot check is the thing only Windows can answer: whether
* Windows, like Wine, gives raw input to a window whose thread does
* not have the focus. */
#include <stdio.h>
#include <stdlib.h>
#include <retro_atomic.h>
#include "input/drivers/winraw_input.c"
/* The frontend, as far as the driver links against it. */
uint8_t g_win32_flags;
ui_window_win32_t main_window;
retro_keybind_set input_config_binds[MAX_USERS];
retro_keybind_set input_autoconf_binds[MAX_USERS];
enum retro_key rarch_keysym_lut[RETROK_LAST];
const struct rarch_key_map rarch_key_map_winraw[] = { { 0, RETROK_UNKNOWN } };
static settings_t stub_settings;
static struct menu_state stub_menu;
settings_t *config_get_ptr(void) { return &stub_settings; }
struct menu_state *menu_state_get_ptr(void) { return &stub_menu; }
void RARCH_LOG(const char *fmt, ...) { (void)fmt; }
void RARCH_DBG(const char *fmt, ...) { (void)fmt; }
void RARCH_ERR(const char *fmt, ...) { (void)fmt; }
void input_config_set_mouse_display_name(unsigned port, const char *name)
{ (void)port; (void)name; }
unsigned input_driver_lightgun_id_convert(unsigned id) { return id; }
bool input_driver_pointer_is_offscreen(int16_t x, int16_t y)
{ (void)x; (void)y; return false; }
void input_keymaps_init_keyboard_lut(const struct rarch_key_map *map)
{ (void)map; }
void joypad_driver_reinit(void *data, const char *name)
{ (void)data; (void)name; }
retro_task_t *task_init(void) { return NULL; }
bool task_queue_push(retro_task_t *task) { (void)task; return false; }
void task_set_flags(retro_task_t *task, uint8_t flags, bool set)
{ (void)task; (void)flags; (void)set; }
bool video_driver_get_viewport_info(struct video_viewport *vp)
{ (void)vp; return false; }
bool video_driver_translate_coord_viewport(struct video_viewport *vp,
int mouse_x, int mouse_y, int16_t *res_x, int16_t *res_y,
int16_t *res_screen_x, int16_t *res_screen_y, bool report_oob)
{
(void)vp; (void)mouse_x; (void)mouse_y; (void)res_x; (void)res_y;
(void)res_screen_x; (void)res_screen_y; (void)report_oob;
return false;
}
uintptr_t video_driver_window_get(void) { return 0; }
void win32_clip_window(bool grab) { (void)grab; }
void win32_hotplug_arm(void) { }
bool win32_hotplug_due(void) { return false; }
/* a key event as the frontend receives it, and on which thread */
#define SEEN_MAX 64
static struct { bool down; unsigned code; uint16_t mod; DWORD tid; } seen[SEEN_MAX];
static unsigned seen_n;
void input_keyboard_event(bool down, unsigned code, uint32_t character,
uint16_t mod, unsigned device)
{
(void)character; (void)device;
if (seen_n < SEEN_MAX)
{
seen[seen_n].down = down;
seen[seen_n].code = code;
seen[seen_n].mod = mod;
seen[seen_n].tid = GetCurrentThreadId();
seen_n++;
}
}
/* the scancode itself, so the test can see which key came through */
enum retro_key input_keymaps_translate_keysym_to_rk(unsigned sym)
{ return (enum retro_key)sym; }
static uint16_t published_mods;
uint16_t win32_get_keyboard_mods(void) { return published_mods; }
/* the controller driver's entry for reports read in bulk */
static unsigned hid_taken;
static DWORD hid_taken_size;
void winraw_joypad_take_hid(HANDLE device, const BYTE *data, DWORD size)
{
(void)device; (void)data;
hid_taken++;
hid_taken_size = size;
}
static unsigned failures;
#define CHECK(cond, ...) do { \
if (!(cond)) { printf(" FAIL "); printf(__VA_ARGS__); printf("\n"); failures++; } \
} while (0)
static winraw_input_t *wr;
/* ---- the "video thread": owns the focused window, pumps it, and is
* where the driver is started from ---------------------------------- */
static HANDLE vt_ready, vt_quit;
static HWND vt_window;
static DWORD vt_tid;
static void *vt_init_result;
static retro_atomic_int_t vt_do_init;
static retro_atomic_int_t vt_do_hook;
static LRESULT CALLBACK plain_proc(HWND w, UINT m, WPARAM wp, LPARAM lp)
{ return DefWindowProcA(w, m, wp, lp); }
static DWORD WINAPI video_thread(LPVOID arg)
{
WNDCLASSA wc;
MSG msg;
(void)arg;
vt_tid = GetCurrentThreadId();
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = plain_proc;
wc.hInstance = GetModuleHandleA(NULL);
wc.lpszClassName = "winraw-poll-test";
RegisterClassA(&wc);
vt_window = CreateWindowExA(0, wc.lpszClassName, "t",
WS_OVERLAPPEDWINDOW | WS_VISIBLE, 10, 10, 200, 100,
NULL, NULL, wc.hInstance, NULL);
if (vt_window)
{
SetForegroundWindow(vt_window);
SetFocus(vt_window);
}
SetEvent(vt_ready);
while (WaitForSingleObject(vt_quit, 2) == WAIT_TIMEOUT)
{
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE))
DispatchMessageA(&msg);
if (retro_atomic_load_acquire_int(&vt_do_init) == 1)
{
vt_init_result = winraw_init("null");
retro_atomic_store_release_int(&vt_do_init, 2);
}
if (retro_atomic_load_acquire_int(&vt_do_hook) == 1)
{
winraw_drain_queue();
retro_atomic_store_release_int(&vt_do_hook, 2);
}
}
if (vt_window)
DestroyWindow(vt_window);
return 0;
}
static void *init_on_video_thread(void)
{
unsigned spins;
vt_init_result = NULL;
retro_atomic_store_release_int(&vt_do_init, 1);
for (spins = 0; spins < 2000
&& retro_atomic_load_acquire_int(&vt_do_init) != 2; spins++)
Sleep(1);
retro_atomic_store_release_int(&vt_do_init, 0);
return vt_init_result;
}
static void hook_on_video_thread(void)
{
unsigned spins;
retro_atomic_store_release_int(&vt_do_hook, 1);
for (spins = 0; spins < 2000
&& retro_atomic_load_acquire_int(&vt_do_hook) != 2; spins++)
Sleep(1);
retro_atomic_store_release_int(&vt_do_hook, 0);
}
static void key(WORD scan, bool down)
{
INPUT in;
memset(&in, 0, sizeof(in));
in.type = INPUT_KEYBOARD;
in.ki.wScan = scan;
in.ki.dwFlags = KEYEVENTF_SCANCODE | (down ? 0 : KEYEVENTF_KEYUP);
SendInput(1, &in, sizeof(in));
}
static unsigned wm_input_waiting(void)
{
MSG msg;
unsigned n = 0;
while (PeekMessageA(&msg, NULL, WM_INPUT, WM_INPUT, PM_REMOVE))
n++;
return n;
}
int main(void)
{
DWORD main_tid = GetCurrentThreadId();
HANDLE vt;
HWND raw_window;
unsigned i;
bool raw_arrives = false;
vt_ready = CreateEventA(NULL, TRUE, FALSE, NULL);
vt_quit = CreateEventA(NULL, TRUE, FALSE, NULL);
vt = CreateThread(NULL, 0, video_thread, NULL, 0, NULL);
WaitForSingleObject(vt_ready, 5000);
if (!vt_window)
printf(" note no display for a window: injected input is not checked\n");
/* ---- the switch off: as it was -------------------------------- */
_putenv("RETROARCH_RAWINPUT_POLL=");
wr = (winraw_input_t*)init_on_video_thread();
CHECK(wr != NULL, "init with the switch off");
if (!wr)
return 1;
CHECK(!wr->poll_drain, "reading in bulk with the switch off");
CHECK(wr->window && GetWindowThreadProcessId(wr->window, NULL) == vt_tid,
"with the switch off the window is not made by init, on init's thread");
CHECK(winraw_drain_tid == 0, "a bulk-read thread is set with the switch off");
winraw_poll(wr);
CHECK(wr->drain_reads == 0, "a bulk read was made with the switch off");
raw_window = wr->window;
/* destroyed by its own thread, as the frontend does */
SetWindowLongPtr(raw_window, GWLP_USERDATA, 0);
PostMessageA(raw_window, WM_CLOSE, 0, 0);
wr->window = NULL;
winraw_free(wr);
printf(" ok switch off: the window is made by init on init's thread, nothing read in bulk\n");
/* ---- the switch on -------------------------------------------- */
_putenv("RETROARCH_RAWINPUT_POLL=1");
wr = (winraw_input_t*)init_on_video_thread();
CHECK(wr != NULL, "init with the switch on");
if (!wr)
return 1;
CHECK(wr->poll_drain && !wr->sink, "not reading in bulk with the switch on");
CHECK(!wr->window, "init made the window");
winraw_poll(wr);
raw_window = wr->window;
CHECK(raw_window && IsWindow(raw_window), "the first poll made no window");
CHECK(GetWindowThreadProcessId(raw_window, NULL) == main_tid
&& main_tid != vt_tid && winraw_drain_tid == main_tid,
"the window belongs to thread %lu; the poll's is %lu, init's %lu",
(unsigned long)GetWindowThreadProcessId(raw_window, NULL),
(unsigned long)main_tid, (unsigned long)vt_tid);
printf(" ok switch on: init makes no window; the first poll does, on the polling thread\n");
/* ---- injected keys, read in bulk by one poll ------------------ */
if (vt_window)
{
unsigned long reads0;
unsigned spins;
/* does raw input for an injected key arrive here at all? */
key(0x1E, true);
for (spins = 0; spins < 100 && !wr->kb_keys[0x1E]; spins++)
{
Sleep(5);
winraw_poll(wr);
}
raw_arrives = wr->kb_keys[0x1E] != 0;
key(0x1E, false);
for (spins = 0; spins < 100 && wr->kb_keys[0x1E]; spins++)
{
Sleep(5);
winraw_poll(wr);
}
seen_n = 0;
if (raw_arrives)
{
/* A, then B with shift held: six reports, nothing pumping on
* this thread while they arrive */
wr->drained = wr->by_message = 0;
reads0 = wr->drain_reads;
key(0x1E, true);
key(0x1E, false);
key(SC_LSHIFT, true);
key(0x30, true);
key(0x30, false);
key(SC_LSHIFT, false);
Sleep(150);
CHECK(seen_n == 0 && !wr->kb_keys[0x30], "something was taken before the poll");
winraw_poll(wr);
CHECK(wr->drained == 6 && wr->by_message == 0,
"%lu reports read in bulk and %lu as messages; 6 were injected",
wr->drained, wr->by_message);
CHECK(wr->drain_reads - reads0 == 1, "it took %lu bulk reads",
wr->drain_reads - reads0);
CHECK(seen_n == 6, "%u key events out of the poll", seen_n);
CHECK(seen_n == 6
&& seen[0].down && seen[0].code == 0x1E && !(seen[0].mod & RETROKMOD_SHIFT)
&& !seen[1].down && seen[1].code == 0x1E
&& seen[2].down && seen[2].code == SC_LSHIFT
&& seen[3].down && seen[3].code == 0x30 && (seen[3].mod & RETROKMOD_SHIFT)
&& !seen[4].down && seen[4].code == 0x30 && (seen[4].mod & RETROKMOD_SHIFT)
&& !seen[5].down && seen[5].code == SC_LSHIFT && !(seen[5].mod & RETROKMOD_SHIFT),
"the key events are not the six injected, in order, with shift on the B");
for (i = 0; i < seen_n; i++)
CHECK(seen[i].tid == main_tid, "event %u was delivered on thread %lu, not the poll's",
i, (unsigned long)seen[i].tid);
CHECK(wm_input_waiting() == 0, "reports read in bulk were left in the queue as messages");
seen_n = 0;
winraw_poll(wr);
CHECK(seen_n == 0, "a second poll delivered them again");
printf(" ok six injected reports: one bulk read, none as a message, six key events from the poll, in order\n");
/* ---- a report the pump gets to first -------------------- */
{
MSG msg;
wr->drained = wr->by_message = 0;
key(0x2E, true);
Sleep(100);
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE))
DispatchMessageA(&msg);
CHECK(wr->by_message == 1 && wr->kb_keys[0x2E],
"a report dispatched as a message was not taken (%lu)", wr->by_message);
CHECK(seen_n == 0, "its key event did not wait for the poll");
winraw_poll(wr);
CHECK(seen_n == 1 && seen[0].down && seen[0].code == 0x2E && seen[0].tid == main_tid,
"its key event did not come out of the poll");
key(0x2E, false);
Sleep(100);
winraw_poll(wr);
seen_n = 0;
printf(" ok a report the pump dispatches first: taken by the callback, its event still from the poll\n");
}
/* ---- the pump's hook ------------------------------------ */
{
wr->drained = 0;
key(0x20, true);
Sleep(100);
hook_on_video_thread();
CHECK(wr->drained == 0 && !wr->kb_keys[0x20],
"the hook read in bulk on a thread that is not the window's");
winraw_drain_queue();
CHECK(wr->drained == 1 && wr->kb_keys[0x20],
"the hook did not read in bulk on the window's thread (%lu)", wr->drained);
CHECK(seen_n == 0, "the hook delivered a key event itself");
key(0x20, false);
Sleep(100);
winraw_poll(wr);
CHECK(seen_n == 2, "%u events after the hook and a poll", seen_n);
seen_n = 0;
printf(" ok the pump's hook: reads in bulk on the window's thread, nothing on another\n");
}
}
else
printf(" note an injected key did not arrive as raw input here: the bulk read is not checked\n");
}
/* ---- a controller's report in a bulk read --------------------- */
{
/* What the loop in winraw_drain() does with a record of this
* type, on a record made here: the payload it hands over is the
* report bytes and their size. */
union { RAWINPUT ri; BYTE bytes[64]; } rec;
RAWHID *hid;
memset(&rec, 0, sizeof(rec));
rec.ri.header.dwType = RIM_TYPEHID;
hid = (RAWHID*)((BYTE*)&rec.ri.data + winraw_wow64_shift());
hid->dwSizeHid = 7;
hid->dwCount = 2;
hid_taken = 0;
winraw_joypad_take_hid(rec.ri.header.hDevice, hid->bRawData,
hid->dwSizeHid * hid->dwCount);
CHECK(hid_taken == 1 && hid_taken_size == 14, "a controller report of 2 x 7 bytes: %u, %lu",
hid_taken, (unsigned long)hid_taken_size);
}
/* ---- modifiers ------------------------------------------------ */
{
published_mods = RETROKMOD_CTRL | RETROKMOD_NUMLOCK;
memset(wr->kb_keys, 0, SC_LAST);
CHECK(winraw_held_mods(wr) == RETROKMOD_NUMLOCK,
"nothing held: %#x", winraw_held_mods(wr));
wr->kb_keys[SC_RSHIFT] = 1;
wr->kb_keys[SC_RALT] = 1;
wr->kb_keys[SC_LSUPER] = 1;
CHECK(winraw_held_mods(wr) == (RETROKMOD_SHIFT | RETROKMOD_ALT
| RETROKMOD_META | RETROKMOD_NUMLOCK),
"right shift, right alt and a windows key held: %#x", winraw_held_mods(wr));
memset(wr->kb_keys, 0, SC_LAST);
published_mods = 0;
printf(" ok modifiers: held keys from the key table, toggles from the published value\n");
}
/* ---- background input ----------------------------------------- */
{
CHECK(!winraw_in_background(wr, RIM_INPUT), "foreground input refused");
CHECK(winraw_in_background(wr, RIM_INPUTSINK), "background input taken");
wr->sink = true;
winraw_focus = false;
CHECK(winraw_in_background(wr, RIM_INPUTSINK),
"a sink took input with the main window unfocused");
winraw_focus = true;
CHECK(!winraw_in_background(wr, RIM_INPUTSINK),
"a sink refused input with the main window focused");
wr->sink = false;
winraw_focus = false;
printf(" ok background input: refused, unless a sink with the main window focused\n");
}
/* ---- free ----------------------------------------------------- */
winraw_free(wr);
CHECK(!IsWindow(raw_window), "free left the window");
CHECK(winraw_drain_tid == 0 && winraw_drain_wr == NULL, "free left the bulk-read thread set");
winraw_drain_queue(); /* nothing to read for, and no driver to touch */
printf(" ok free: the window is gone and the pump's hook has nothing to do\n");
/* ---- restarted, as it is with every video driver restart ------ */
{
DWORD t0 = GetTickCount();
unsigned ok = 0;
for (i = 0; i < 20; i++)
{
HWND w;
wr = (winraw_input_t*)init_on_video_thread();
if (!wr)
break;
winraw_poll(wr);
w = wr->window;
if (w && GetWindowThreadProcessId(w, NULL) == main_tid)
{
winraw_free(wr);
if (!IsWindow(w))
ok++;
}
else
winraw_free(wr);
}
CHECK(ok == 20, "%u of 20 restarts made their window on the polling thread and took it down", ok);
printf(" ok 20 restarts: each window made by the poll, each gone after free (%lu ms)\n",
(unsigned long)(GetTickCount() - t0));
}
SetEvent(vt_quit);
WaitForSingleObject(vt, 5000);
if (failures)
{
printf("FAIL winraw_poll_test: %u\n", failures);
return 1;
}
printf("PASS winraw_poll_test%s\n", raw_arrives ? "" : " (without injected input)");
return 0;
}
+13
View File
@@ -131,10 +131,23 @@ static void ui_application_win32_dispatch(MSG *msg)
}
}
#if defined(HAVE_WINRAWINPUT) && !defined(_XBOX) && _WIN32_WINNT >= 0x0501 && !defined(__WINRT__)
extern void winraw_drain_queue(void);
#define WIN32_RAW_INPUT_DRAIN() winraw_drain_queue()
#else
#define WIN32_RAW_INPUT_DRAIN() ((void)0)
#endif
static void ui_application_win32_process_events(void)
{
MSG msg;
/* Raw input reports waiting on this thread are read in bulk before
* the loop below would dispatch them one WM_INPUT at a time. Does
* nothing unless the raw input driver was asked to read that way
* and its window is this thread's. */
WIN32_RAW_INPUT_DRAIN();
while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
ui_application_win32_dispatch(&msg);