From 31d8eda9987703c64fc63f4ddb4088cf3be5b7cc Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Sun, 27 Nov 2022 11:43:01 -0500 Subject: [PATCH] x11: Fix redraw on window resize. It turns out that relying on expose events to trigger redraw of certain tiles when enlarging is not actually a good idea. No expose events will be generated if the new portion of the window is obscured while resizing, which means tiles along the bottom or right edge of the game/goal areas may not get redrawn when they should be. We can avoid the redundant drawing on resize another way. Instead of redrawing immediately in response to resize or expose events, we just mark those tiles as needing update and only actually redraw after a short delay, which is hopefully enough to catch the expose events directly generated by the resize. --- src/motif.h | 7 +++++++ src/motif_ui.c | 50 ++++------------------------------------------- src/x11.c | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/motif.h b/src/motif.h index 9064788..28e1b76 100644 --- a/src/motif.h +++ b/src/motif.h @@ -34,6 +34,11 @@ struct app_state { XtIntervalId timer_tick; + XtIntervalId render_tick; + XtWorkProcId render_proc; + uint_fast32_t render_game_mask; + uint_fast16_t render_goal_mask; + /* If true, the goal will be displayed over the main play area. */ int view_goal_on_game; @@ -53,5 +58,7 @@ void x11_initialize(struct app_state *state, Widget shell); void x11_redraw_icon(struct app_state *state, Widget shell); void x11_redraw_goal(struct app_state *state, uint_fast32_t mask); void x11_redraw_game(struct app_state *state, uint_fast32_t mask); +void x11_queue_render(struct app_state *state, uint_fast32_t game_mask, + uint_fast16_t goal_mask); #endif diff --git a/src/motif_ui.c b/src/motif_ui.c index fefcfed..718375c 100644 --- a/src/motif_ui.c +++ b/src/motif_ui.c @@ -268,55 +268,14 @@ expose_mask(int rect_x, int rect_y, int rect_w, int rect_h, & board_left((rect_x + rect_w - 1) / tile_w); } -/* - * Calculate which tiles need to be redrawn after resizing. This is the - * tiles which (at the new size) are fully contained in the previous area. - * - * When shrinking, this is all tiles, but when enlarging, the generated - * expose events will trigger drawing of the new area. - */ -static uint_fast32_t resize_helper(Widget w, Dimension *oldsz, int gridsz) -{ - Dimension new_w, new_h, common_w, common_h; - - if (!XtIsRealized(w)) - return 0; - - XtVaGetValues(w, XmNwidth, &new_w, XmNheight, &new_h, (char *)NULL); - common_w = MIN(new_w, oldsz[0]); - common_h = MIN(new_h, oldsz[1]); - oldsz[0] = new_w; - oldsz[1] = new_h; - new_w /= gridsz; - new_h /= gridsz; - - /* Round down to multiple of tile dimensions */ - common_w = common_w / new_w * new_w; - common_h = common_h / new_h * new_h; - - if (new_w > 0 && new_h > 0) - return expose_mask(0, 0, common_w, common_h, new_w, new_h); - return 0; -} - static void game_resize(Widget w, void *data, void *cb_data) { - struct app_state *state = data; - uint_fast32_t mask; - - mask = resize_helper(w, state->game_sz, 5); - if (mask) - x11_redraw_game(data, mask); + x11_queue_render(data, -1, 0); } static void goal_resize(Widget w, void *data, void *cb_data) { - struct app_state *state = data; - uint_fast32_t mask; - - mask = resize_helper(w, state->goal_sz, 3); - if (mask) - x11_redraw_goal(data, mask); + x11_queue_render(data, 0, -1); } static void game_expose(Widget w, void *data, void *cb_data) @@ -339,8 +298,7 @@ static void game_expose(Widget w, void *data, void *cb_data) */ mask = gp[0] | gp[1] | gp[2]; mask &= expose_mask(e->x, e->y, e->width, e->height, width, height); - - x11_redraw_game(state, mask); + x11_queue_render(state, mask, 0); } static void goal_expose(Widget w, void *data, void *cb_data) @@ -355,7 +313,7 @@ static void goal_expose(Widget w, void *data, void *cb_data) return; mask = expose_mask(e->x, e->y, e->width, e->height, width, height); - x11_redraw_goal(data, mask); + x11_queue_render(data, 0, mask); } void ui_initialize(struct app_state *state, Widget shell) diff --git a/src/x11.c b/src/x11.c index 874bfdc..6954f06 100644 --- a/src/x11.c +++ b/src/x11.c @@ -361,3 +361,56 @@ void x11_redraw_game(struct app_state *state, uint_fast32_t mask) mask >>= 1; } } + +/* + * Deferred redraw of tiles after resize/expose to avoid redundant drawing. + * + * Record any tiles that need to be redrawn due to resizes or expose events, + * then, after a short delay, perform all the accumulated redraws at once. + * + * This is implemented using both a work proc and a timeout, because it seems + * that rendering directly inside the timeout callback gives poor results: + * possibly redrawing outdated intermediate positions during "fast" resizes + * long after the resizing has stopped. This does not happen when drawing + * from a work proc. + */ +static Boolean do_render(void *data) +{ + struct app_state *state = data; + + x11_redraw_goal(state, state->render_goal_mask); + x11_redraw_game(state, state->render_game_mask); + + state->render_goal_mask = state->render_game_mask = 0; + state->render_proc = 0; + state->render_tick = 0; + return True; +} + +static void start_render(void *data, XtIntervalId *id) +{ + struct app_state *state = data; + XtAppContext app; + + if (state->render_proc) + return; + + app = XtWidgetToApplicationContext(state->game); + state->render_proc = XtAppAddWorkProc(app, do_render, state); +} + +void x11_queue_render(struct app_state *state, uint_fast32_t game_mask, + uint_fast16_t goal_mask) +{ + uint_fast32_t changed = 0; + XtAppContext app; + + changed |= state->render_game_mask |= game_mask; + changed |= state->render_goal_mask |= goal_mask; + + if (state->render_tick || !changed) + return; + + app = XtWidgetToApplicationContext(state->game); + state->render_tick = XtAppAddTimeOut(app, 3, start_render, state); +} -- 2.43.2