events: reduce processing overhead

This is mostly aimed at speeding up TAISEI_SKIP_TO_BOOKMARK mode

- Store global handlers in a dynamic array
- Merge global and local handlers more efficiently, and only do it once
  per events_poll() call
- Do not pump SDL events while skipping through stages
- Avoid expensive system call when pushing custom events to the queue
- Process SDL events in batches; do not pump after every event
- Simplify handling of EventPriority enum - EPRIO_DEFAULT equals to 0,
  no remapping required
This commit is contained in:
Andrei Alexeyev 2021-05-05 20:32:56 +03:00
parent dd33bd0e4f
commit d126eba2d2
No known key found for this signature in database
GPG key ID: 72D26128040B9690
5 changed files with 155 additions and 202 deletions

View file

@ -90,7 +90,3 @@ void _dynarray_filter(dynarray_size_t sizeof_element, DynamicArray *darr, dynarr
p += sizeof_element;
}
}
#if 0
#endif

View file

@ -14,16 +14,9 @@
#include "video.h"
#include "gamepad.h"
typedef struct EventHandlerContainer {
LIST_INTERFACE(struct EventHandlerContainer);
EventHandler *handler;
} EventHandlerContainer;
typedef LIST_ANCHOR(EventHandlerContainer) EventHandlerList;
static hrtime_t keyrepeat_paused_until;
static EventHandlerList global_handlers;
static int global_handlers_lock = 0;
static DYNAMIC_ARRAY(EventHandler) global_handlers;
static DYNAMIC_ARRAY(SDL_Event) deferred_events;
uint32_t sdl_first_user_event;
@ -31,6 +24,8 @@ uint32_t sdl_first_user_event;
static void events_register_default_handlers(void);
static void events_unregister_default_handlers(void);
#define MAX_ACTIVE_HANDLERS 32
/*
* Public API
*/
@ -55,14 +50,12 @@ void events_shutdown(void) {
dynarray_free_data(&deferred_events);
#ifdef DEBUG
if(global_handlers.first) {
log_warn(
"Someone didn't unregister their handler. "
"Clean up after yourself, I'm not your personal maid. "
"Hint: ASan or valgrind can probably determine the culprit."
);
}
dynarray_foreach_elem(&global_handlers, EventHandler *h, {
log_warn("Global event handler was not unregistered: %p", UNION_CAST(EventHandlerProc, void*, h->proc));
});
#endif
dynarray_free_data(&global_handlers);
}
static bool events_invoke_handler(SDL_Event *event, EventHandler *handler) {
@ -77,182 +70,38 @@ static bool events_invoke_handler(SDL_Event *event, EventHandler *handler) {
return false;
}
static int handler_container_prio_func(List *h) {
return ((EventHandlerContainer*)h)->handler->priority;
}
static EventPriority real_priority(EventPriority prio) {
if(prio == EPRIO_DEFAULT) {
return EPRIO_DEFAULT_REMAP;
}
assert(prio < NUM_EPRIOS);
return prio;
}
static EventHandlerContainer* ehandler_wrap_container(EventHandler *handler) {
EventHandlerContainer *c = calloc(1, sizeof(*c));
c->handler = handler;
return c;
}
typedef struct EventHandlerSortHelper {
EventHandler *handler;
uint idx;
} EventHandlerSortHelper;
static int ehandler_sort_cmp(const void *a, const void *b) {
const EventHandlerSortHelper *h0 = a;
const EventHandlerSortHelper *h1 = b;
EventPriority p0 = real_priority(h0->handler->priority);
EventPriority p1 = real_priority(h1->handler->priority);
int diff = (p0 > p1) - (p1 > p0);
if(diff == 0) {
// stabilize sort
diff = (h0->idx > h1->idx) - (h1->idx > h0->idx);
}
assert(diff != 0);
return diff;
}
static bool _events_invoke_handlers(SDL_Event *event, EventHandlerContainer *h_list, EventHandler *h_array) {
// invoke handlers from two sources (a list and an array) in the correct order according to priority
// list items take precedence
//
// assumptions:
// h_list is sorted by priority
// h_array is in arbitrary order and terminated with a NULL-proc entry
bool result = false;
if(h_list && !h_array) {
// case 1 (simplest): we have a list and no custom handlers
for(EventHandlerContainer *c = h_list; c; c = c->next) {
if(c->handler->_private.removal_pending) {
continue;
}
if((result = events_invoke_handler(event, c->handler))) {
break;
}
}
return result;
}
if(h_list && h_array) {
// case 2 (suboptimal): we have both a list and a disordered array; need to do some actual work
// if you want to optimize this be my guest
uint cnt = 0, idx =0;
for(EventHandlerContainer *c = h_list; c; c = c->next) {
++cnt;
}
for(EventHandler *h = h_array; h->proc; ++h) {
++cnt;
}
EventHandlerSortHelper merged[cnt];
for(EventHandlerContainer *c = h_list; c; c = c->next, ++idx) {
merged[idx].handler = c->handler;
merged[idx].idx = idx;
}
for(EventHandler *h = h_array; h->proc; ++h, ++idx) {
merged[idx].handler = h;
merged[idx].idx = idx;
}
qsort(merged, cnt, sizeof(*merged), ehandler_sort_cmp);
for(int i = 0; i < cnt; ++i) {
EventHandler *e = merged[i].handler;
if(e->_private.removal_pending) {
continue;
}
if((result = events_invoke_handler(event, e))) {
break;
}
}
}
if(!h_list && h_array) {
// case 3 (unlikely): we don't have a list for some reason (no global handlers?), but there are custom handlers
for(EventHandler *h = h_array; h->proc; ++h) {
if((result = events_invoke_handler(event, h))) {
break;
}
}
return result;
}
// case 4 (unlikely): huh? okay then
return result;
}
static bool events_invoke_handlers(SDL_Event *event, EventHandlerContainer *h_list, EventHandler *h_array) {
++global_handlers_lock;
bool result = _events_invoke_handlers(event, h_list, h_array);
if(--global_handlers_lock == 0) {
for(EventHandlerContainer *c = global_handlers.first, *next; c; c = next) {
next = c->next;
if(c->handler->_private.removal_pending) {
free(c->handler);
free(alist_unlink(&global_handlers, c));
}
}
}
return result;
static inline int prio_index(EventPriority prio) {
return prio - EPRIO_FIRST;
}
void events_register_handler(EventHandler *handler) {
assert(handler->proc != NULL);
EventHandler *handler_alloc = malloc(sizeof(EventHandler));
memcpy(handler_alloc, handler, sizeof(EventHandler));
assert(handler->priority >= EPRIO_FIRST);
assert(handler->priority <= EPRIO_LAST);
if(handler_alloc->priority == EPRIO_DEFAULT) {
handler_alloc->priority = EPRIO_DEFAULT_REMAP;
}
*dynarray_append(&global_handlers) = *handler;
assert(handler_alloc->priority > EPRIO_DEFAULT);
alist_insert_at_priority_tail(
&global_handlers,
ehandler_wrap_container(handler_alloc),
handler_alloc->priority,
handler_container_prio_func
);
// don't bother sorting, since most of the time we will need to re-sort it
// together with local handlers when polling
}
static bool hfilter_remove_pending(const void *pelem, void *ignored) {
const EventHandler *h = pelem;
return !h->_private.removal_pending;
}
void events_unregister_handler(EventHandlerProc proc) {
for(EventHandlerContainer *c = global_handlers.first, *next; c; c = next) {
EventHandler *h = c->handler;
next = c->next;
dynarray_foreach_elem(&global_handlers, EventHandler *h, {
if(h->proc == proc) {
if(global_handlers_lock) {
assert(global_handlers_lock > 0);
c->handler->_private.removal_pending = true;
return;
}
free(c->handler);
free(alist_unlink(&global_handlers, c));
return;
h->_private.removal_pending = true;
break;
}
});
if(global_handlers_lock) {
assert(global_handlers_lock > 0);
} else {
dynarray_filter(&global_handlers, hfilter_remove_pending, NULL);
}
}
@ -278,17 +127,103 @@ static void events_apply_flags(EventFlags flags) {
}
}
static int enqueue_event_handlers(int max, EventHandler *queue[max], EventHandler local_handlers[]) {
// counting sort!
int cnt = 0;
int pcount[NUM_EPRIOS] = { 0 };
EventHandler *temp[max];
assert(global_handlers.num_elements <= max);
dynarray_foreach_elem(&global_handlers, EventHandler *h, {
++pcount[prio_index(h->priority)];
temp[cnt++] = h;
});
if(local_handlers) {
for(EventHandler *h = local_handlers; h->proc; ++h) {
int pidx = prio_index(h->priority);
assert((uint)pidx < ARRAY_SIZE(pcount));
++pcount[pidx];
temp[cnt++] = h;
assert(cnt < max);
}
}
for(int i = 0, total = 0; i < NUM_EPRIOS; ++i) {
int t = total;
total += pcount[i];
pcount[i] = t;
}
for(int i = 0; i < cnt; ++i) {
EventHandler *h = temp[i];
int p = prio_index(h->priority);
queue[pcount[p]] = h;
++pcount[p];
}
return cnt;
}
static void push_event(SDL_Event *e) {
/*
* NOTE: The SDL_PushEvent() function is a wrapper around SDL_PeepEvents that also sets the
* timestamp field and calls the event filter function and event watchers. We don't use any of
* that, and setting the timestamp involves an expensive system call, so avoid it.
*/
if(UNLIKELY(SDL_PeepEvents(e, 1, SDL_ADDEVENT, 0, 0) <= 0)) {
log_sdl_error(LOG_ERROR, "SDL_PeepEvents");
}
}
void events_poll(EventHandler *handlers, EventFlags flags) {
SDL_Event event;
events_apply_flags(flags);
events_emit(TE_FRAME, 0, NULL, NULL);
while(SDL_PollEvent(&event)) {
events_invoke_handlers(&event, global_handlers.first, handlers);
++global_handlers_lock;
EventHandler *hqueue[MAX_ACTIVE_HANDLERS];
int hqueue_size = enqueue_event_handlers(ARRAY_SIZE(hqueue), hqueue, handlers);
for(;;) {
if(!(flags & EFLAG_NOPUMP)) {
SDL_PumpEvents();
}
SDL_Event events[8];
int nevents = SDL_PeepEvents(events, ARRAY_SIZE(events), SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT);
if(UNLIKELY(nevents < 0)) {
log_sdl_error(LOG_ERROR, "SDL_PeepEvents");
}
if(nevents == 0) {
break;
}
for(SDL_Event *e = events, *end = events + nevents; e < end; ++e) {
for(int i = 0; i < hqueue_size; ++i) {
EventHandler *h = NOT_NULL(hqueue[i]);
if(h->_private.removal_pending) {
continue;
}
if(events_invoke_handler(e, h)) {
break;
}
}
}
}
if(--global_handlers_lock == 0) {
dynarray_filter(&global_handlers, hfilter_remove_pending, NULL);
}
dynarray_foreach_elem(&deferred_events, SDL_Event *evt, {
SDL_PushEvent(evt);
push_event(evt);
});
deferred_events.num_elements = 0;
@ -309,7 +244,7 @@ void events_emit(TaiseiEvent type, int32_t code, void *data1, void *data2) {
event.user.data1 = data1;
event.user.data2 = data2;
SDL_PushEvent(&event);
push_event(&event);
}
void events_defer(SDL_Event *evt) {

View file

@ -51,24 +51,28 @@ typedef enum {
} TaiseiEvent;
typedef enum {
EPRIO_DEFAULT = 0,
// from highest to lowest
// feel free to add new prios as needed, just don't randomly reorder stuff
EPRIO_SYSTEM, // for events not associated with user input
EPRIO_SYSTEM = -4, // for events not associated with user input
EPRIO_TRANSLATION, // for translating raw input events into higher level Taisei events
EPRIO_CAPTURE, // for capturing raw user input before it's further processed
EPRIO_HOTKEYS, // for global keybindings
EPRIO_NORMAL, // for everything else
NUM_EPRIOS
EPRIO_DEFAULT = EPRIO_NORMAL,
EPRIO_FIRST = EPRIO_SYSTEM,
EPRIO_LAST = EPRIO_NORMAL,
NUM_EPRIOS = EPRIO_LAST - EPRIO_FIRST + 1,
} EventPriority;
static_assert_nomsg(EPRIO_DEFAULT == 0);
typedef enum {
EFLAG_MENU = (1 << 0),
EFLAG_GAME = (1 << 1),
EFLAG_TEXT = (1 << 2),
EFLAG_NOPUMP = (1 << 3),
} EventFlags;
typedef enum {
@ -76,8 +80,6 @@ typedef enum {
INDEV_GAMEPAD,
} InputDevice;
#define EPRIO_DEFAULT_REMAP EPRIO_NORMAL
// if the this returns true, the event won't be passed down to lower priority handlers
typedef bool (*EventHandlerProc)(SDL_Event *event, void *arg);

View file

@ -287,7 +287,14 @@ bool gamepad_update_devices(void) {
return true;
}
static inline void set_events_state(int state) {
SDL_JoystickEventState(state);
SDL_GameControllerEventState(state);
}
void gamepad_init(void) {
set_events_state(SDL_IGNORE);
if(!config_get_int(CONFIG_GAMEPAD_ENABLED) || gamepad.initialized) {
return;
}
@ -309,7 +316,7 @@ void gamepad_init(void) {
.priority = EPRIO_TRANSLATION,
});
SDL_GameControllerEventState(SDL_ENABLE);
set_events_state(SDL_ENABLE);
}
void gamepad_shutdown(void) {
@ -330,7 +337,7 @@ void gamepad_shutdown(void) {
free(gamepad.devices);
SDL_GameControllerEventState(SDL_IGNORE);
set_events_state(SDL_IGNORE);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
memset(&gamepad, 0, sizeof(gamepad));

View file

@ -422,11 +422,19 @@ static bool stage_handle_bgm_change(SDL_Event *evt, void *a) {
}
static void stage_input(void) {
events_poll((EventHandler[]){
{ .proc = stage_input_handler_gameplay },
{ .proc = stage_handle_bgm_change, .event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED) },
{NULL}
}, EFLAG_GAME);
if(stage_is_skip_mode()) {
events_poll((EventHandler[]){
{ .proc = stage_handle_bgm_change, .event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED) },
{NULL}
}, EFLAG_NOPUMP);
} else {
events_poll((EventHandler[]){
{ .proc = stage_input_handler_gameplay },
{ .proc = stage_handle_bgm_change, .event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED) },
{NULL}
}, EFLAG_GAME);
}
player_fix_input(&global.plr);
player_applymovement(&global.plr);
}
@ -632,6 +640,11 @@ void stage_finish(int gameover) {
log_debug("Stage cleared %u times now", p->num_cleared);
}
}
if(gameover == GAMEOVER_SCORESCREEN && stage_is_skip_mode()) {
// don't get stuck in an infinite loop
taisei_quit();
}
}
static void stage_preload(void) {