protocols: honor tearing hints and fractional scale

Drive the output commit's async-flip flag from the shown surface's
tearing hint (a real tearing flip on DRM; correctly ignored on
headless/nested), with per-object hint tracking that requests a frame
on change. Notify each surface of the output scale at claim so
clients never guess preferred_scale.

wltest gains --tearing and --fractional-out (plus linking the two
protocol stubs it now uses); smoke tests cover the async commit
transition and the notified scale value.
This commit is contained in:
Andrew Hamilton 2026-09-04 10:21:30 +10:00
parent 8a71a062ce
commit 50a660860d
5 changed files with 228 additions and 18 deletions

95
kick.c
View file

@ -375,9 +375,9 @@ struct kick_server {
becomes a copyable frame. */
struct wlr_screencopy_manager_v1 *screencopy_mgr;
/* Tearing (wp-tearing-control-v1) + fractional-scale (wp-fractional-scale):
both are advertised for client convenience. kick drives a vsynced scene
and fixed 1.0 scale, so nothing is honoured: the hint object is only
logged (DEBUG), and fractional scale has nothing to notify. */
the shown surface's hint drives the commit's async-flip flag (see
apply_tearing_hint), and each surface learns the output scale at
claim. Per-object tearing listeners live in kick_tearing. */
struct wlr_tearing_control_manager_v1 *tearing_mgr;
struct wl_listener tearing_new_object;
struct wlr_fractional_scale_manager_v1 *fractional_scale_mgr;
@ -3630,6 +3630,14 @@ static void claim_xdg_window(struct kick_tab *tab, struct kick_window *w,
set_app_id events re-sync the handles). */
foreign_handle_create(w);
/* Tell the surface the output scale up front: clients that wait for
preferred_scale before sizing assets would otherwise guess. The
scale is fixed per session, so one notification covers the surface. */
if (server.fractional_scale_mgr && toplevel->base->surface) {
double scale = server.output ? (double)server.output->scale : 1.0;
wlr_fractional_scale_v1_notify_scale(toplevel->base->surface, scale);
}
w->request_maximize.notify = handle_request_maximize;
wl_signal_add(&toplevel->events.request_maximize, &w->request_maximize);
w->request_fullscreen.notify = handle_request_fullscreen;
@ -5951,21 +5959,82 @@ static void handle_idle_new_inhibitor(struct wl_listener *listener, void *data)
}
/* ===================== Tearing + fractional-scale =====================
Both are pass-through wlroots managers that exist only to be advertised:
- wp-tearing-control-v1 lets a surface request unthrottled presentation.
kick drives a fully vsynced scene (no page-flip bypass), so the hint is
only logged (DEBUG); nothing skips vsync.
- wp-fractional-scale-v1 lets clients learn a non-integer scale. kick's
output runs at scale 1.0 and never changes it at runtime, so there is
nothing to notify via wlr_fractional_scale_v1_notify_scale. */
The shown tab's focused surface hint drives the output commit's
async-flip flag: on DRM this becomes a real tearing page flip (game
tabs pair it with content-type=game); headless/nested outputs ignore
the flag, correctly, since a tear cannot pass through the host.
- wp-fractional-scale-v1 tells a surface the output scale. kick's
output scale is fixed per session, so each surface is notified once
at claim (from the live output scale, covering HiDPI DRM too). */
struct kick_tearing {
struct wlr_tearing_control_v1 *tc;
struct wl_listener set_hint;
struct wl_listener destroy;
};
static void handle_tearing_hint(struct wl_listener *listener, void *data) {
struct kick_tearing *kt = wl_container_of(listener, kt, set_hint);
(void)data;
kick_log(KICK_LOG_DEBUG, "[tearing] surface %p hint=%d\n",
(void *)kt->tc->surface, (int)kt->tc->current);
/* The commit path re-reads the hint every frame, but a hint change
alone produces no frame: request one so async takes effect (or
stops) promptly. */
kick_request_redraw();
}
static void handle_tearing_destroy(struct wl_listener *listener, void *data) {
struct kick_tearing *kt = wl_container_of(listener, kt, destroy);
(void)data;
wl_list_remove(&kt->set_hint.link);
wl_list_remove(&kt->destroy.link);
free(kt);
}
static void handle_tearing_new_object(struct wl_listener *listener,
void *data) {
(void)listener;
struct wlr_tearing_control_v1 *tc = data;
struct kick_tearing *kt = calloc(1, sizeof(*kt));
if (!kt)
return;
kt->tc = tc;
kt->set_hint.notify = handle_tearing_hint;
wl_signal_add(&tc->events.set_hint, &kt->set_hint);
kt->destroy.notify = handle_tearing_destroy;
wl_signal_add(&tc->events.destroy, &kt->destroy);
kick_log(KICK_LOG_DEBUG, "[tearing] surface %p hint=%d\n",
(void *)tc->surface, (int)tc->current);
}
/* Whether the shown tab's focused surface asked for async presentation. */
static bool shown_surface_wants_tearing(void) {
if (!server.tearing_mgr)
return false;
struct kick_window *w = tab_current_window(server.active_tab);
struct wlr_surface *s = window_surface(w);
if (!s)
return false;
return wlr_tearing_control_manager_v1_surface_hint_from_surface(
server.tearing_mgr, s) ==
WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC;
}
/* Apply the shown surface's hint to an output commit. Logs transitions
only, so steady vsync (the common case) stays quiet at debug level. */
static void apply_tearing_hint(struct wlr_output_state *state) {
bool async = shown_surface_wants_tearing();
state->tearing_page_flip = async;
static bool last_async = false;
if (async != last_async) {
last_async = async;
struct kick_window *w = tab_current_window(server.active_tab);
kick_log(KICK_LOG_DEBUG, "[tearing] %s app=%d\n", async ? "async" : "vsync",
w ? tab_index(w->tab) : -1);
}
}
/* ===================== Keyboard shortcuts inhibit =====================
A fullscreen game/VM asks that the compositor stop processing its keyboard
shortcuts so they reach the client instead. Honored only while the
@ -9898,6 +9967,7 @@ static void render_standalone(void) {
dump_standalone_buffer(state.buffer);
}
apply_tearing_hint(&state);
if (!wlr_output_commit_state(server.output, &state))
kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n");
@ -9981,6 +10051,7 @@ static void render_and_present(void) {
blit_scene_buffer(state.buffer);
apply_tearing_hint(&state);
if (!wlr_output_commit_state(server.output, &state))
kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n");
@ -10998,9 +11069,9 @@ int main(int argc, char *argv[]) {
runs the whole capture flow itself off the output commit path. */
server.screencopy_mgr = wlr_screencopy_manager_v1_create(server.wl_display);
/* Tearing-control + fractional-scale: advertised for client convenience
(games, HiDPI surfaces). Nothing is honoured beyond the DEBUG log on
a new tearing object. */
/* Tearing-control + fractional-scale: the shown surface's tearing hint
drives the commit's async-flip flag (see apply_tearing_hint), and
surfaces learn the output scale at claim. */
server.tearing_mgr = wlr_tearing_control_manager_v1_create(server.wl_display, 1);
server.tearing_new_object.notify = handle_tearing_new_object;
wl_signal_add(&server.tearing_mgr->events.new_object,