mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 17:28:49 -10:00
win32: keep the run loop going during size/move and menu loops
A title bar drag, a border resize, or an open menu enters a modal loop inside DefWindowProc that holds the thread until the user lets go, so no frame runs and no audio is written for the length of the drag. The loop still dispatches posted messages. For its duration a helper thread sleeps at the content rate on the high resolution timer and posts a tick, and each tick runs one iteration. The iteration is told that an external clock is pacing it, through a new RUNLOOP_PACE_EXTERNAL source: the frame-limit sleep, frame delay, and the no-window wait then yield to that clock, so the iteration returns as soon as the frame is out and the modal loop has the mouse back within a frame's work rather than a frame's period. Content and audio hold their rate at any refresh the core reports. Where no thread can be had, WM_TIMER drives the ticks instead. The thread is created once and parks between sessions, so a drag does not leave retro_sleep()'s per-thread timer handle behind. Three things guard the nested iteration: a flag drops a tick pulled out by the iteration's own pump, the pump leaves mouse messages in the queue while a tick runs since the modal loop has the mouse captured, and a quit that an iteration accepted is not repeated on later ticks. Threaded video is left alone, as the window then belongs to the video thread and the run loop is not blocked. Everything used exists on Windows 95; message and timer constants are defined locally where an SDK lacks them.
This commit is contained in:
1 parent
a417ac8022
commit
91920289e3
6 files changed
+362
-17
No files matched your search
@@ -36,6 +36,11 @@
|
||||
#include <retro_miscellaneous.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <retro_atomic.h>
|
||||
#include <retro_timers.h>
|
||||
#include <features/features_cpu.h>
|
||||
#ifdef HAVE_THREADS
|
||||
#include <rthreads/rthreads.h>
|
||||
#endif
|
||||
#ifdef HAVE_DYLIB
|
||||
#include <dynamic/dylib.h>
|
||||
#endif
|
||||
@@ -612,6 +617,254 @@ uint16_t win32_get_keyboard_mods(void)
|
||||
return (uint16_t)retro_atomic_load_acquire_int(&win32_kb_mods);
|
||||
}
|
||||
|
||||
#if !defined(_XBOX)
|
||||
/* A title bar drag, a border resize, or an open menu enters a modal
|
||||
* loop inside DefWindowProc that does not return until the user lets
|
||||
* go. The run loop lives on this thread, so for that whole time no
|
||||
* frame runs, no audio is written, and the frame limiter falls behind
|
||||
* by the length of the drag.
|
||||
*
|
||||
* The loop does dispatch posted messages, so for its duration a helper
|
||||
* thread sleeps at the content rate on the high resolution timer and
|
||||
* posts a tick; the tick runs one iteration with RUNLOOP_PACE_EXTERNAL
|
||||
* set, which makes the iteration return as soon as the frame is out
|
||||
* instead of sleeping until the next one. Content and audio keep their
|
||||
* rate, and the loop has the mouse back within a frame's work, not a
|
||||
* frame's period. Where there is no thread to be had the fallback is
|
||||
* WM_TIMER, whose coarse cadence still beats a freeze.
|
||||
*
|
||||
* Every API here dates from Windows 95; the constants an older SDK
|
||||
* lacks are defined below. */
|
||||
|
||||
/* Chosen not to collide with anything else that might use timers or
|
||||
* WM_APP messages on the main window. */
|
||||
#define WIN32_MODAL_TIMER_ID 0x5241
|
||||
#define WM_RA_MODAL_TICK (WM_APP + 0x52)
|
||||
|
||||
#ifndef WM_APP
|
||||
#define WM_APP 0x8000
|
||||
#endif
|
||||
#ifndef USER_TIMER_MINIMUM
|
||||
#define USER_TIMER_MINIMUM 0x0000000A
|
||||
#endif
|
||||
#ifndef WM_ENTERMENULOOP
|
||||
#define WM_ENTERMENULOOP 0x0211
|
||||
#endif
|
||||
#ifndef WM_EXITMENULOOP
|
||||
#define WM_EXITMENULOOP 0x0212
|
||||
#endif
|
||||
#ifndef WM_ENTERSIZEMOVE
|
||||
#define WM_ENTERSIZEMOVE 0x0231
|
||||
#endif
|
||||
#ifndef WM_EXITSIZEMOVE
|
||||
#define WM_EXITSIZEMOVE 0x0232
|
||||
#endif
|
||||
|
||||
typedef struct
|
||||
{
|
||||
#ifdef HAVE_THREADS
|
||||
sthread_t *thread;
|
||||
/* Signalled to start a clock session; the thread parks on it
|
||||
* between sessions rather than being created per drag, which
|
||||
* would also leave retro_sleep()'s per-thread timer handle behind
|
||||
* each time. Both events auto-reset. */
|
||||
HANDLE wake;
|
||||
HANDLE parked;
|
||||
#endif
|
||||
HWND hwnd;
|
||||
/* Set once a tick is posted and cleared when it is consumed, so a
|
||||
* main thread that falls behind gets one message, not a backlog. */
|
||||
retro_atomic_int_t pending;
|
||||
retro_atomic_int_t stop;
|
||||
retro_atomic_int_t quit;
|
||||
/* Size/move and menu loops can nest (a menu opened from the system
|
||||
* menu of a window being sized is the one case), so arm on the
|
||||
* first entry and disarm on the last exit. */
|
||||
uint8_t depth;
|
||||
} win32_modal_t;
|
||||
|
||||
static win32_modal_t win32_modal;
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
static void win32_modal_clock(void *data)
|
||||
{
|
||||
win32_modal_t *m = (win32_modal_t*)data;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
retro_time_t next;
|
||||
|
||||
WaitForSingleObject(m->wake, INFINITE);
|
||||
if (retro_atomic_load_acquire_int(&m->quit))
|
||||
break;
|
||||
|
||||
next = cpu_features_get_time_usec();
|
||||
while (!retro_atomic_load_acquire_int(&m->stop))
|
||||
{
|
||||
/* core_hz is written on the main thread and only read here;
|
||||
* a torn read costs one oddly timed tick, nothing worse. */
|
||||
retro_time_t period = runloop_content_frame_time_us(
|
||||
video_state_get_ptr()->core_hz);
|
||||
retro_time_t now = cpu_features_get_time_usec();
|
||||
retro_time_t wait;
|
||||
|
||||
next += period;
|
||||
wait = next - now;
|
||||
if (wait > 0)
|
||||
retro_sleep_us((unsigned)wait);
|
||||
else
|
||||
next = now; /* fell behind: no burst, just resume */
|
||||
|
||||
if (retro_atomic_cas_int(&m->pending, 0, 1))
|
||||
PostMessage(m->hwnd, WM_RA_MODAL_TICK, 0, 0);
|
||||
}
|
||||
|
||||
SetEvent(m->parked);
|
||||
}
|
||||
}
|
||||
|
||||
/* One thread for the life of the process, created on first use. NULL
|
||||
* on any failure, in which case the WM_TIMER fallback is used. */
|
||||
static sthread_t *win32_modal_thread_get(win32_modal_t *m)
|
||||
{
|
||||
if (m->thread)
|
||||
return m->thread;
|
||||
|
||||
if (!m->wake && !(m->wake = CreateEvent(NULL, FALSE, FALSE, NULL)))
|
||||
return NULL;
|
||||
if (!m->parked && !(m->parked = CreateEvent(NULL, FALSE, FALSE, NULL)))
|
||||
return NULL;
|
||||
|
||||
retro_atomic_store_release_int(&m->quit, 0);
|
||||
return (m->thread = sthread_create(win32_modal_clock, m));
|
||||
}
|
||||
#endif
|
||||
|
||||
static void win32_modal_enter(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
if (m->depth++)
|
||||
return;
|
||||
|
||||
/* With threaded video this window, and so this loop, belong to the
|
||||
* video thread; the run loop is elsewhere and is not blocked. */
|
||||
if (video_driver_is_threaded())
|
||||
return;
|
||||
|
||||
m->hwnd = hwnd;
|
||||
retro_atomic_store_release_int(&m->pending, 0);
|
||||
retro_atomic_store_release_int(&m->stop, 0);
|
||||
runloop_state_get_ptr()->pace_external = true;
|
||||
g_win32_flags |= WIN32_CMN_FLAG_MODAL_TIMER;
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
if (win32_modal_thread_get(m))
|
||||
{
|
||||
SetEvent(m->wake);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
SetTimer(hwnd, WIN32_MODAL_TIMER_ID, USER_TIMER_MINIMUM, NULL);
|
||||
}
|
||||
|
||||
/* Stops the clock whatever the nesting depth: the last exit message,
|
||||
* a quit from inside a tick, and a window going away mid-drag all
|
||||
* end here. */
|
||||
static void win32_modal_stop(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
m->depth = 0;
|
||||
if (!(g_win32_flags & WIN32_CMN_FLAG_MODAL_TIMER))
|
||||
return;
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
if (m->thread)
|
||||
{
|
||||
/* The clock is inside a sleep of at most one frame; once it has
|
||||
* parked it cannot post again until the next wake. */
|
||||
retro_atomic_store_release_int(&m->stop, 1);
|
||||
WaitForSingleObject(m->parked, INFINITE);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
KillTimer(hwnd, WIN32_MODAL_TIMER_ID);
|
||||
|
||||
/* A tick that was posted before the stop is still in the queue;
|
||||
* the flag clear below makes it a no-op when it arrives. */
|
||||
g_win32_flags &= ~WIN32_CMN_FLAG_MODAL_TIMER;
|
||||
runloop_state_get_ptr()->pace_external = false;
|
||||
}
|
||||
|
||||
static void win32_modal_exit(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
if (!m->depth || --m->depth)
|
||||
return;
|
||||
win32_modal_stop(hwnd);
|
||||
}
|
||||
|
||||
/* The window is going away. Any session is ended and the thread, if
|
||||
* one was ever started, is joined. */
|
||||
static void win32_modal_deinit(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
win32_modal_stop(hwnd);
|
||||
#ifdef HAVE_THREADS
|
||||
if (m->thread)
|
||||
{
|
||||
retro_atomic_store_release_int(&m->quit, 1);
|
||||
SetEvent(m->wake);
|
||||
sthread_join(m->thread);
|
||||
m->thread = NULL;
|
||||
}
|
||||
if (m->wake)
|
||||
{
|
||||
CloseHandle(m->wake);
|
||||
m->wake = NULL;
|
||||
}
|
||||
if (m->parked)
|
||||
{
|
||||
CloseHandle(m->parked);
|
||||
m->parked = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void win32_modal_tick(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
int ret;
|
||||
|
||||
retro_atomic_store_release_int(&m->pending, 0);
|
||||
|
||||
if (!(g_win32_flags & WIN32_CMN_FLAG_MODAL_TIMER))
|
||||
return;
|
||||
|
||||
/* The iteration pumps this thread's queue, and a tick pulled out
|
||||
* there would land here again, inside the iteration it belongs to. */
|
||||
if (g_win32_flags & WIN32_CMN_FLAG_MODAL_TICK)
|
||||
return;
|
||||
|
||||
/* A quit that was already accepted is reported by the main loop's
|
||||
* own next iteration; repeating it here would run the shutdown
|
||||
* work once per tick until the loop lets go. */
|
||||
if (runloop_state_get_ptr()->flags & RUNLOOP_FLAG_SHUTDOWN_INITIATED)
|
||||
return;
|
||||
|
||||
g_win32_flags |= WIN32_CMN_FLAG_MODAL_TICK;
|
||||
ret = runloop_iterate();
|
||||
task_queue_check();
|
||||
g_win32_flags &= ~WIN32_CMN_FLAG_MODAL_TICK;
|
||||
|
||||
if (ret == -1)
|
||||
win32_modal_stop(hwnd);
|
||||
}
|
||||
#endif /* !defined(_XBOX) */
|
||||
|
||||
static LRESULT CALLBACK wnd_proc_common(
|
||||
bool *quit, HWND hwnd, UINT message,
|
||||
WPARAM wparam, LPARAM lparam)
|
||||
@@ -657,12 +910,32 @@ static LRESULT CALLBACK wnd_proc_common(
|
||||
case WM_CLOSE:
|
||||
case WM_DESTROY:
|
||||
case WM_QUIT:
|
||||
#if !defined(_XBOX)
|
||||
win32_modal_deinit(hwnd);
|
||||
#endif
|
||||
g_win32_flags |= WIN32_CMN_FLAG_QUIT;
|
||||
*quit = true;
|
||||
/* fall-through */
|
||||
case WM_MOVE:
|
||||
win32_save_position();
|
||||
break;
|
||||
#if !defined(_XBOX)
|
||||
case WM_ENTERSIZEMOVE:
|
||||
case WM_ENTERMENULOOP:
|
||||
win32_modal_enter(hwnd);
|
||||
break;
|
||||
case WM_EXITSIZEMOVE:
|
||||
case WM_EXITMENULOOP:
|
||||
win32_modal_exit(hwnd);
|
||||
break;
|
||||
case WM_RA_MODAL_TICK:
|
||||
win32_modal_tick(hwnd);
|
||||
break;
|
||||
case WM_TIMER:
|
||||
if (wparam == WIN32_MODAL_TIMER_ID)
|
||||
win32_modal_tick(hwnd);
|
||||
break;
|
||||
#endif
|
||||
case WM_SIZE:
|
||||
/* Do not send resize message if we minimize. */
|
||||
if ( wparam != SIZE_MAXHIDE
|
||||
@@ -879,6 +1152,14 @@ static LRESULT CALLBACK wnd_proc_common_internal(HWND hwnd,
|
||||
case WM_SIZE:
|
||||
case WM_GETMINMAXINFO:
|
||||
case WM_COMMAND:
|
||||
#if !defined(_XBOX)
|
||||
case WM_ENTERSIZEMOVE:
|
||||
case WM_EXITSIZEMOVE:
|
||||
case WM_ENTERMENULOOP:
|
||||
case WM_EXITMENULOOP:
|
||||
case WM_TIMER:
|
||||
case WM_RA_MODAL_TICK:
|
||||
#endif
|
||||
#ifdef HAVE_THREADS
|
||||
case WM_BROWSER_OPEN_RESULT:
|
||||
case WM_BROWSER_CANCELLED:
|
||||
@@ -964,6 +1245,14 @@ static LRESULT CALLBACK wnd_proc_winraw_common_internal(HWND hwnd,
|
||||
case WM_SIZE:
|
||||
case WM_GETMINMAXINFO:
|
||||
case WM_COMMAND:
|
||||
#if !defined(_XBOX)
|
||||
case WM_ENTERSIZEMOVE:
|
||||
case WM_EXITSIZEMOVE:
|
||||
case WM_ENTERMENULOOP:
|
||||
case WM_EXITMENULOOP:
|
||||
case WM_TIMER:
|
||||
case WM_RA_MODAL_TICK:
|
||||
#endif
|
||||
#ifdef HAVE_THREADS
|
||||
case WM_BROWSER_OPEN_RESULT:
|
||||
case WM_BROWSER_CANCELLED:
|
||||
@@ -1175,6 +1464,14 @@ static LRESULT CALLBACK wnd_proc_common_dinput_internal(HWND hwnd,
|
||||
case WM_SIZE:
|
||||
case WM_GETMINMAXINFO:
|
||||
case WM_COMMAND:
|
||||
#if !defined(_XBOX)
|
||||
case WM_ENTERSIZEMOVE:
|
||||
case WM_EXITSIZEMOVE:
|
||||
case WM_ENTERMENULOOP:
|
||||
case WM_EXITMENULOOP:
|
||||
case WM_TIMER:
|
||||
case WM_RA_MODAL_TICK:
|
||||
#endif
|
||||
#ifdef HAVE_THREADS
|
||||
case WM_BROWSER_OPEN_RESULT:
|
||||
case WM_BROWSER_CANCELLED:
|
||||
|
||||
@@ -70,7 +70,13 @@ enum win32_common_flags
|
||||
WIN32_CMN_FLAG_TASKBAR_CREATED = (1 << 2),
|
||||
WIN32_CMN_FLAG_RESTORE_DESKTOP = (1 << 3),
|
||||
WIN32_CMN_FLAG_INITED = (1 << 4),
|
||||
WIN32_CMN_FLAG_SWAP_MOUSE_BTNS = (1 << 5)
|
||||
WIN32_CMN_FLAG_SWAP_MOUSE_BTNS = (1 << 5),
|
||||
/* The window is inside a modal size/move or menu loop owned by
|
||||
* DefWindowProc, and a timer is driving the run loop from within
|
||||
* it. */
|
||||
WIN32_CMN_FLAG_MODAL_TIMER = (1 << 6),
|
||||
/* A run loop iteration started from that timer is in progress. */
|
||||
WIN32_CMN_FLAG_MODAL_TICK = (1 << 7)
|
||||
};
|
||||
|
||||
extern uint8_t g_win32_flags;
|
||||
|
||||
@@ -8307,6 +8307,8 @@ end:
|
||||
* reset there made it read NONE on every frame. Paths that return
|
||||
* before this block set it themselves. */
|
||||
runloop_st->pace = RUNLOOP_PACE_NONE;
|
||||
if (runloop_st->pace_external)
|
||||
runloop_st->pace |= RUNLOOP_PACE_EXTERNAL;
|
||||
/* How long the last iteration actually took, smoothed. One clock
|
||||
* read on a path that already takes several, and the only way to
|
||||
* tell a source that is holding the loop from one that merely says
|
||||
@@ -8407,8 +8409,10 @@ end:
|
||||
{
|
||||
/* One frame of content time, so a window that comes back is
|
||||
* noticed within a frame and the core keeps its own rate while
|
||||
* hidden. */
|
||||
retro_sleep_us((unsigned)runloop_content_frame_time_us(video_st->core_hz));
|
||||
* hidden. Under an external clock the caller is already back
|
||||
* within a frame. */
|
||||
if (!(runloop_st->pace & RUNLOOP_PACE_EXTERNAL))
|
||||
retro_sleep_us((unsigned)runloop_content_frame_time_us(video_st->core_hz));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -8456,7 +8460,11 @@ end:
|
||||
const retro_time_t to_sleep = to_sleep_us;
|
||||
#endif
|
||||
|
||||
if (to_sleep > 0)
|
||||
/* Under an external clock the sleep is the caller's; the
|
||||
* schedule is re-anchored below so it does not carry a
|
||||
* backlog into the first frame after the clock lets go. */
|
||||
if ( to_sleep > 0
|
||||
&& !(runloop_st->pace & RUNLOOP_PACE_EXTERNAL))
|
||||
{
|
||||
/* Combat jitter a bit. */
|
||||
runloop_st->frame_limit_last_time += frame_limit_min;
|
||||
@@ -8477,9 +8485,12 @@ end:
|
||||
}
|
||||
}
|
||||
|
||||
/* Frame delay */
|
||||
if ( !(input_st->flags & INP_FLAG_NONBLOCKING)
|
||||
|| (runloop_st->flags & RUNLOOP_FLAG_FASTMOTION))
|
||||
/* Frame delay. Not under an external clock: it is a sleep before
|
||||
* the core runs, and the caller has to be back before the next
|
||||
* tick. */
|
||||
if ( !(runloop_st->pace & RUNLOOP_PACE_EXTERNAL)
|
||||
&& ( !(input_st->flags & INP_FLAG_NONBLOCKING)
|
||||
|| (runloop_st->flags & RUNLOOP_FLAG_FASTMOTION)))
|
||||
video_frame_delay(video_st, settings);
|
||||
|
||||
/* Set paused state after x frames */
|
||||
|
||||
@@ -353,6 +353,9 @@ struct runloop
|
||||
* cross-thread race, so reusing it would undo that reasoning for
|
||||
* no gain. This is main-thread only. */
|
||||
bool content_closing;
|
||||
/* An external clock is calling runloop_iterate(); see
|
||||
* RUNLOOP_PACE_EXTERNAL. Main-thread only, same reasoning. */
|
||||
bool pace_external;
|
||||
};
|
||||
|
||||
/* Frame pacing sources.
|
||||
@@ -383,7 +386,13 @@ enum runloop_pace_source
|
||||
RUNLOOP_PACE_AUDIO = (1 << 1), /* audio crystal: write blocks */
|
||||
RUNLOOP_PACE_SCANLINE = (1 << 2), /* display: vblank-locked wait */
|
||||
RUNLOOP_PACE_TIMER = (1 << 3), /* CPU counter: frame-limit sleep */
|
||||
RUNLOOP_PACE_NOWINDOW = (1 << 4) /* nothing to present to: wait */
|
||||
RUNLOOP_PACE_NOWINDOW = (1 << 4), /* nothing to present to: wait */
|
||||
/* The host is calling runloop_iterate() at the content rate from
|
||||
* somewhere it must return quickly to - a Win32 modal size/move
|
||||
* loop, for one. The frame-limit sleep, frame delay, and the
|
||||
* no-window wait yield to that clock rather than stack on it. Set
|
||||
* from runloop_state_t::pace_external. */
|
||||
RUNLOOP_PACE_EXTERNAL = (1 << 5)
|
||||
};
|
||||
|
||||
/* The three pacing decisions the runloop makes every iteration, here
|
||||
|
||||
@@ -64,7 +64,7 @@ static void test_gap_predicate(void)
|
||||
int nb, fm;
|
||||
unsigned engaged = 0;
|
||||
|
||||
for (pace = 0; pace < 32; pace++)
|
||||
for (pace = 0; pace < 64; pace++)
|
||||
for (nb = 0; nb < 2; nb++)
|
||||
for (fm = 0; fm < 2; fm++)
|
||||
{
|
||||
|
||||
+30
-8
@@ -85,19 +85,41 @@ static void* ui_application_win32_initialize(void)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void ui_application_win32_dispatch(MSG *msg)
|
||||
{
|
||||
bool translated_accelerator = main_window.hwnd == msg->hwnd && TranslateAccelerator(msg->hwnd, window_accelerators, msg) != 0;
|
||||
|
||||
if (!translated_accelerator)
|
||||
{
|
||||
TranslateMessage(msg);
|
||||
DispatchMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_application_win32_process_events(void)
|
||||
{
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
|
||||
{
|
||||
bool translated_accelerator = main_window.hwnd == msg.hwnd && TranslateAccelerator(msg.hwnd, window_accelerators, &msg) != 0;
|
||||
|
||||
if (!translated_accelerator)
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
/* Called from a run loop iteration that a modal size/move or menu
|
||||
* loop is driving from its timer (win32_common.c). That loop has
|
||||
* the mouse captured and reads its own movement and button-up from
|
||||
* the queue; taking those here would leave it tracking nothing.
|
||||
* Everything else - keyboard, raw input, WM_TIMER, posted commands
|
||||
* - is dispatched as usual. Ranges are numeric so that they do not
|
||||
* move with the SDK's idea of WM_MOUSELAST. */
|
||||
if (g_win32_flags & WIN32_CMN_FLAG_MODAL_TICK)
|
||||
{
|
||||
while (PeekMessage(&msg, 0, 0x0000, 0x009F, PM_REMOVE))
|
||||
ui_application_win32_dispatch(&msg);
|
||||
while (PeekMessage(&msg, 0, 0x00B0, 0x01FF, PM_REMOVE))
|
||||
ui_application_win32_dispatch(&msg);
|
||||
while (PeekMessage(&msg, 0, 0x0210, 0xFFFF, PM_REMOVE))
|
||||
ui_application_win32_dispatch(&msg);
|
||||
return;
|
||||
}
|
||||
|
||||
while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
|
||||
ui_application_win32_dispatch(&msg);
|
||||
}
|
||||
|
||||
static ui_application_t ui_application_win32 = {
|
||||
|
||||
Reference in new issue
Block a user