From d8164ce315e8508a429264741104d18f836d972a Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Fri, 4 Sep 2026 10:52:16 +1000 Subject: [PATCH] protocols: expose output-management, honor resizes standalone Advertise zwlr_output_manager_v1 with the single head, published on setup and re-published from the commit path whenever the size lands (resizes apply lazily, so the commit -- not the request -- is where the new size becomes observable). Client sizes apply standalone through the normal resize path (tabs reflow, swapchain follows); nested refuses anything but the live size since the host owns the geometry. Disabling, scale and transform changes are refused; superseded configs are destroyed after feedback. Standalone output_resize now owns the canvas size so client resizes reach the lazy modeset. wltest gains --output-mgmt; smoke tests cover the global, head listing, and an 800x600 apply with tab reflow. --- README.md | 9 + kick.c | 171 +++++ meson.build | 8 + tests/test_smoke.py | 79 +++ tests/wltest.c | 293 ++++++++- .../wlr-output-management-unstable-v1.xml | 601 ++++++++++++++++++ 6 files changed, 1158 insertions(+), 3 deletions(-) create mode 100644 wlr-protocols/wlr-output-management-unstable-v1.xml diff --git a/README.md b/README.md index a957b93..d7e90f7 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,15 @@ artwork shows where clients provide it. The guesser stays as the fallback icon: clearing the icon drops back to guessing, and windows that never set one behave exactly as before. +## Output management (`wlr-output-management`) + +Kick advertises `zwlr_output_manager_v1`, so display tools (kanshi, +wdisplays) can list the single head and request sizes. Standalone honors +custom sizes through the normal resize path (tabs reflow, clients learn +the new size); nested refuses anything but the live size, because the +host window owns the geometry there. Disabling, scale and transform +changes are refused everywhere: one fixed output per session. + ## Logging (`KICK_LOG`) Quiet by default: a plain launch prints nothing to the launching terminal diff --git a/kick.c b/kick.c index 8638646..1705663 100644 --- a/kick.c +++ b/kick.c @@ -96,6 +96,7 @@ #include #include #include +#include #include #include #include @@ -393,6 +394,13 @@ struct kick_server { struct wlr_output_power_manager_v1 *output_power_mgr; struct wl_listener output_power_set_mode; bool output_powered; + /* Output management (kanshi/wdisplays reconfigure the output): the + single server output is published as a head; client size requests + are honored standalone and refused nested (the host owns the size). + Scale/transform changes and disabling are unsupported. */ + struct wlr_output_manager_v1 *output_mgr; + struct wl_listener output_mgr_apply; + struct wl_listener output_mgr_test; /* Gamma control (night-light tools like wlsunset/gammastep): a client sets a per-output ramp; wlroots rejects outputs with no LUT (e.g. the headless output) itself, so the handler only commits ramps for @@ -870,6 +878,9 @@ static const char *foreign_str(const char *s); static struct kick_window *tab_current_window(struct kick_tab *tab); /* Toplevel-icon body lives with the overlay code; called from teardown. */ static void icon_proto_release(struct kick_window *w); +/* Output-management publish lives with the modeset handlers; called from + output setup and every resize. */ +static void output_mgmt_publish(void); static struct kick_tab *pick_successor(struct kick_tab *tab, bool prefer_right); static void remove_tab(struct kick_tab *tab); @@ -2479,6 +2490,15 @@ static void output_resize(int width, int height) { if (height < 50) height = 50; if (!server.output || !server.allocator) return; + /* Standalone has no host window tracking the size: the canvas follows + the request so the lazy modeset in apply_pending_output_resize + picks it up at render (nested is a no-op: the args already are the + host window's size). */ + if (server.standalone) { + host.width = width; + host.height = height; + } + /* The new size is applied to the output lazily at render time (see apply_pending_output_resize); here only the scene geometry and the clients are told, so configure-time stays free of modesets and @@ -2706,6 +2726,10 @@ server.output = output; server.overlay_damage_rect = wlr_scene_rect_create( &server.scene->tree, w, h, (float[]){ 0.0f, 0.0f, 0.0f, 0.0f }); } + + /* Publish the fresh output to management clients (kanshi/wdisplays + learn the head and its modes/size on bind). */ + output_mgmt_publish(); } /* Nearest live tab, scanning right first (prefer_right) or left first. @@ -6193,6 +6217,141 @@ static void handle_output_power_set_mode(struct wl_listener *listener, kick_request_redraw(); } +/* ===================== Output management ===================== + kanshi/wdisplays-style clients list the single head and request sizes. + Standalone honors custom sizes through the normal resize path (tabs + reflow, swapchain follows lazily at render); nested refuses anything + but the live size because the host window owns the geometry. Anything + beyond size (disabling, scale, transform) is refused everywhere: one + fixed output per session. Successful applies republish so listening + clients converge. */ + +/* Broadcast the live output as the manager's current configuration; + wlroots creates/updates/destroys heads from it and only notifies on + change. No-op until both manager and output exist. */ +static void output_mgmt_publish(void) { + if (!server.output_mgr || !server.output) + return; + struct wlr_output_configuration_v1 *config = + wlr_output_configuration_v1_create(); + if (!config) + return; + if (!wlr_output_configuration_head_v1_create(config, server.output)) { + wlr_output_configuration_v1_destroy(config); + return; + } + wlr_output_manager_v1_set_configuration(server.output_mgr, config); +} + +/* Re-publish after a commit changed the output size. Resizes land + lazily (host configures, client applies), so the commit -- not the + request -- is where the new size becomes observable. */ +static void output_mgmt_sync(void) { + if (!server.output_mgr || !server.output) + return; + static int last_w = -1, last_h = -1; + if (server.output->width == last_w && server.output->height == last_h) + return; + last_w = server.output->width; + last_h = server.output->height; + output_mgmt_publish(); +} + +/* The config head addressing our output, if the client included it. */ +static struct wlr_output_configuration_head_v1 *mgmt_head_for_output( + struct wlr_output_configuration_v1 *config) { + if (!server.output) + return NULL; + struct wlr_output_configuration_head_v1 *h; + wl_list_for_each(h, &config->heads, link) { + if (h->state.output == server.output) + return h; + } + return NULL; +} + +/* Requested size, preferring an explicit mode over a custom one. */ +static void mgmt_head_size(struct wlr_output_configuration_head_v1 *h, + int *w, int *hh) { + if (h->state.mode) { + *w = h->state.mode->width; + *hh = h->state.mode->height; + } else { + *w = h->state.custom_mode.width; + *hh = h->state.custom_mode.height; + } +} + +/* Whether a client config is one we can honor: our output, still + enabled, same scale/transform, sane size. Nested additionally + requires the live size (the host owns it). */ +static bool mgmt_config_ok(struct wlr_output_configuration_v1 *config, + int *w, int *hh) { + struct wlr_output_configuration_head_v1 *h = + mgmt_head_for_output(config); + if (!h || !h->state.enabled || !server.output) + return false; + mgmt_head_size(h, w, hh); + if (*w < 50 || *hh < 50 || *w > 16384 || *hh > 16384) + return false; + if (h->state.scale != (float)server.output->scale || + h->state.transform != server.output->transform) + return false; + if (!server.standalone && + (*w != server.output->width || *hh != server.output->height)) + return false; + return true; +} + +static void handle_output_mgr_test(struct wl_listener *listener, + void *data) { + (void)listener; + struct wlr_output_configuration_v1 *config = data; + int w = 0, hh = 0; + bool ok = mgmt_config_ok(config, &w, &hh); + if (ok && server.standalone) { + /* Honest modeset probe on top of the sanity check. */ + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_custom_mode(&state, w, hh, 0); + ok = wlr_output_test_state(server.output, &state); + wlr_output_state_finish(&state); + } + kick_log(KICK_LOG_DEBUG, "[output] test %dx%d %s\n", w, hh, + ok ? "ok" : "failed"); + if (ok) + wlr_output_configuration_v1_send_succeeded(config); + else + wlr_output_configuration_v1_send_failed(config); + wlr_output_configuration_v1_destroy(config); +} + +static void handle_output_mgr_apply(struct wl_listener *listener, + void *data) { + (void)listener; + struct wlr_output_configuration_v1 *config = data; + int w = 0, hh = 0; + bool ok = mgmt_config_ok(config, &w, &hh); + if (ok && server.standalone) { + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_custom_mode(&state, w, hh, 0); + ok = wlr_output_test_state(server.output, &state); + wlr_output_state_finish(&state); + } + if (ok) { + if (server.standalone) + output_resize(w, hh); + output_mgmt_publish(); + kick_log(KICK_LOG_INFO, "[output] apply %dx%d\n", w, hh); + wlr_output_configuration_v1_send_succeeded(config); + } else { + kick_log(KICK_LOG_INFO, "[output] apply %dx%d refused\n", w, hh); + wlr_output_configuration_v1_send_failed(config); + } + wlr_output_configuration_v1_destroy(config); +} + /* Gamma control: apply the client's ramp to the single output (or reset it when the control went away: wlroots emits set_gamma with NULL then). A failed commit fails the control so the client (wlsunset) retries later @@ -10063,6 +10222,8 @@ static void render_standalone(void) { apply_tearing_hint(&state); if (!wlr_output_commit_state(server.output, &state)) kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n"); + else + output_mgmt_sync(); frame_done: struct timespec now; @@ -10147,6 +10308,8 @@ static void render_and_present(void) { apply_tearing_hint(&state); if (!wlr_output_commit_state(server.output, &state)) kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n"); + else + output_mgmt_sync(); frame_done: struct timespec now; @@ -11189,6 +11352,14 @@ int main(int argc, char *argv[]) { wl_signal_add(&server.output_power_mgr->events.set_mode, &server.output_power_set_mode); + /* Output management: publish the single head; client resizes apply + standalone and are refused nested (see the section). */ + server.output_mgr = wlr_output_manager_v1_create(server.wl_display); + server.output_mgr_apply.notify = handle_output_mgr_apply; + wl_signal_add(&server.output_mgr->events.apply, &server.output_mgr_apply); + server.output_mgr_test.notify = handle_output_mgr_test; + wl_signal_add(&server.output_mgr->events.test, &server.output_mgr_test); + /* Gamma control: night-light clients set the output ramp (see handle_gamma_set); wlroots itself fails controls for outputs with no LUT, so headless/nested clients learn it is unsupported. */ diff --git a/meson.build b/meson.build index c626f6d..ab61a7d 100644 --- a/meson.build +++ b/meson.build @@ -257,6 +257,12 @@ protocols = { wl_protocols_dir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', 'xdg-toplevel-icon-v1', ], + # Output management (kanshi/wdisplays reconfigure the output): + # wlroots extension, vendored like the other wlr-protocols XMLs. + 'output-management': [ + 'wlr-protocols/wlr-output-management-unstable-v1.xml', + 'wlr-output-management-unstable-v1', + ], # Content hints (photo/video/game) + single-pixel buffers (1x1 solid # color without a shm pool). Both are staging protocols from the # system wayland-protocols, like wlroots itself uses. @@ -396,6 +402,7 @@ executable('wltest', code_by_name['xdg-dialog'], code_by_name['pointer-gestures'], code_by_name['toplevel-icon'], + code_by_name['output-management'], client_headers['xdg-shell'], client_headers['cursor-shape'], client_headers['wlr-layer-shell-unstable-v1'], @@ -420,6 +427,7 @@ executable('wltest', client_headers['tearing-control'], client_headers['fractional-scale'], client_headers['toplevel-icon'], + client_headers['output-management'], dependencies: [dependency('wayland-client'), dependency('xkbcommon')]) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 5a257b2..1610aa0 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1516,6 +1516,85 @@ def test_standalone_headless_toplevel_icon_preferred(tmp_path): s.close() +def test_standalone_headless_output_mgmt_globals(tmp_path): + """The standalone compositor advertises zwlr_output_manager_v1 so + display tools can list and reconfigure the output.""" + out = tmp_path / "globals.txt" + s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out)) + try: + deadline = time.monotonic() + 15.0 + while time.monotonic() < deadline: + if out.exists(): + break + time.sleep(0.1) + assert out.exists(), "wltest --globals-out never wrote its file" + names = {line.split()[0] for line in out.read_text().splitlines()} + assert "zwlr_output_manager_v1" in names, \ + f"missing output-mgmt global; advertised: {sorted(names)}" + finally: + s.close() + + +def test_standalone_headless_output_mgmt_list(tmp_path): + """A management client learns the head (name + live size); the plain + color tab keeps the session up after the lister exits.""" + out = tmp_path / "om.out" + s = _spawn_standalone( + {}, "%s --output-mgmt --output-mgmt-out '%s'" % (WL, out), + "%s '#ff0000'" % WL) + try: + s.wait_any_active() + deadline = time.monotonic() + 15.0 + while time.monotonic() < deadline: + if out.exists(): + break + time.sleep(0.1) + assert out.exists(), "output-mgmt lister never wrote its file" + assert re.search(r"^head \S+ \d+x\d+$", out.read_text(), + re.MULTILINE), \ + f"no head line in: {out.read_text()!r}" + assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}" + finally: + s.close() + + +def test_standalone_headless_output_mgmt_apply(tmp_path): + """Applying 800x600 through output-management resizes the session: + the client sees before/applied/after and a tab commits at 800x600.""" + out = tmp_path / "om.out" + s = _spawn_standalone( + {}, + "%s --output-mgmt --output-mgmt-out '%s' --output-mgmt-size 800x600" + % (WL, out), + "%s '#ff0000'" % WL) + try: + s.wait_any_active() + deadline = time.monotonic() + 20.0 + while time.monotonic() < deadline: + if out.exists() and "after " in out.read_text(): + break + time.sleep(0.2) + text = out.read_text() if out.exists() else "" + assert "before " in text, f"no before line in: {text!r}" + assert "applied" in text, f"apply not acked in: {text!r}" + m = re.search(r"^after \S+ (\d+)x(\d+)$", text, re.MULTILINE) + assert m and (m.group(1), m.group(2)) == ("800", "600"), \ + f"no 800x600 after line in: {text!r}" + # A live tab reflows into the new output size. + deadline = time.monotonic() + 15.0 + geom = None + while time.monotonic() < deadline: + if re.search(r"\[diag\] commit app=\d+ geom=800x600", + s.read_log()): + geom = True + break + time.sleep(0.2) + assert geom, "no tab committed at 800x600" + assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}" + finally: + s.close() + + def test_standalone_headless_gestures_globals(tmp_path): """The standalone compositor advertises zwp_pointer_gestures_v1 so touchpad clients can bind swipe/pinch/hold.""" diff --git a/tests/wltest.c b/tests/wltest.c index 5073892..723a183 100644 --- a/tests/wltest.c +++ b/tests/wltest.c @@ -43,6 +43,7 @@ #include "tearing-control-v1-client.h" #include "fractional-scale-v1-client.h" #include "xdg-toplevel-icon-v1-client.h" +#include "wlr-output-management-unstable-v1-client.h" /* Minimal deterministic xdg toplevel test client: shows a solid-color shm buffer and runs until killed. Used by test.sh to give the smoke test two @@ -152,6 +153,7 @@ static struct xdg_activation_v1 *activation_mgr; static struct xdg_wm_dialog_v1 *dialog_mgr; static struct zwp_pointer_gestures_v1 *gestures_mgr; static struct xdg_toplevel_icon_manager_v1 *toplevel_icon_mgr; +static struct zwlr_output_manager_v1 *output_mgr; static struct wp_tearing_control_manager_v1 *tearing_mgr; static struct wp_fractional_scale_manager_v1 *fractional_mgr; static struct zwp_text_input_manager_v3 *text_input_mgr; @@ -839,6 +841,11 @@ static void registry_global(void *data, struct wl_registry *reg, uint32_t name, toplevel_icon_mgr = wl_registry_bind(reg, name, &xdg_toplevel_icon_manager_v1_interface, version < 1 ? version : 1); + } else if (strcmp(interface, + zwlr_output_manager_v1_interface.name) == 0) { + output_mgr = wl_registry_bind(reg, name, + &zwlr_output_manager_v1_interface, + version < 4 ? version : 4); } else if (strcmp(interface, wp_tearing_control_manager_v1_interface.name) == 0) { tearing_mgr = wl_registry_bind(reg, name, @@ -1701,10 +1708,255 @@ static void gesture_bind_all(void) { zwp_pointer_gesture_hold_v1_add_listener(ho, &hold_listener, NULL); } +/* --- Output management (wlr-output-management) --- + --output-mgmt --output-mgmt-out [--output-mgmt-size WxH]: + list the head (name + current size) into ; with a size, apply + it as a custom mode first and record before/after/applied. No window + is opened. */ +static bool output_mgmt_mode; +static const char *output_mgmt_out;static int output_mgmt_w, output_mgmt_h; +static bool output_mgmt_want_size; +static struct zwlr_output_head_v1 *om_head; +static char om_name[64]; +static bool om_applied; +static struct { + struct zwlr_output_mode_v1 *res; + int w, h; +} om_modes[32]; +static int om_nmodes; +static struct zwlr_output_mode_v1 *om_current; +static const struct zwlr_output_head_v1_listener om_head_listener; +static const struct zwlr_output_mode_v1_listener om_mode_listener; + +static void om_note(const char *fmt, ...) { + if (!output_mgmt_out) + return; + FILE *f = fopen(output_mgmt_out, "a"); + if (!f) + return; + va_list ap; + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fputc('\n', f); + fclose(f); +} + +static int om_mode_size(struct zwlr_output_mode_v1 *res, int *w, int *h) { + for (int i = 0; i < om_nmodes; i++) + if (om_modes[i].res == res) { + *w = om_modes[i].w; + *h = om_modes[i].h; + return 0; + } + return -1; +} + +static void om_report_head(const char *tag) { + int w = 0, h = 0; + if (om_mode_size(om_current, &w, &h) == 0) + om_note("%s %s %dx%d", tag, om_name[0] ? om_name : "?", w, h); + else + om_note("%s %s ?", tag, om_name[0] ? om_name : "?"); +} + +static void om_config_succeeded(void *data, + struct zwlr_output_configuration_v1 *cfg) { + (void)data; (void)cfg; + om_note("applied"); + om_applied = true; +} + +static void om_config_failed(void *data, + struct zwlr_output_configuration_v1 *cfg) { + (void)data; (void)cfg; + om_note("failed"); + exit(0); +} + +static void om_config_cancelled(void *data, + struct zwlr_output_configuration_v1 *cfg) { + (void)data; (void)cfg; + /* Superseded by a newer configuration (e.g. a concurrent modeset): + wait for the fresh done and retry with its serial, bounded. */ + static int retries; + if (++retries > 3) { + om_note("cancelled"); + exit(1); + } + om_applied = false; +} + +static const struct zwlr_output_configuration_v1_listener om_config_listener = { + .succeeded = om_config_succeeded, + .failed = om_config_failed, + .cancelled = om_config_cancelled, +}; + +static void om_mgr_head(void *data, struct zwlr_output_manager_v1 *mgr, + struct zwlr_output_head_v1 *head) { + (void)data; (void)mgr; + if (!om_head) { + om_head = head; + zwlr_output_head_v1_add_listener(head, &om_head_listener, NULL); + } +} + +static void om_head_name(void *data, struct zwlr_output_head_v1 *head, + const char *name) { + (void)data; (void)head; + snprintf(om_name, sizeof(om_name), "%s", name ? name : ""); +} + +static void om_noop_head_str(void *data, struct zwlr_output_head_v1 *head, + const char *s) { + (void)data; (void)head; (void)s; +} + +static void om_noop_head_u32(void *data, struct zwlr_output_head_v1 *head, + uint32_t v) { + (void)data; (void)head; (void)v; +} + +static void om_noop_head_i32x2(void *data, struct zwlr_output_head_v1 *head, + int32_t a, int32_t b) { + (void)data; (void)head; (void)a; (void)b; +} + +static void om_noop_head_i32(void *data, struct zwlr_output_head_v1 *head, + int32_t v) { + (void)data; (void)head; (void)v; +} + +static void om_head_mode(void *data, struct zwlr_output_head_v1 *head, + struct zwlr_output_mode_v1 *mode) { + (void)data; (void)head; + if (om_nmodes < 32) { + om_modes[om_nmodes].res = mode; + om_modes[om_nmodes].w = 0; + om_modes[om_nmodes].h = 0; + om_nmodes++; + zwlr_output_mode_v1_add_listener(mode, &om_mode_listener, NULL); + } +} + +static void om_head_current_mode(void *data, + struct zwlr_output_head_v1 *head, + struct zwlr_output_mode_v1 *mode) { + (void)data; (void)head; + om_current = mode; +} + +static void om_head_finished(void *data, struct zwlr_output_head_v1 *head) { + (void)data; (void)head; +} + +static const struct zwlr_output_head_v1_listener om_head_listener = { + .name = om_head_name, + .description = om_noop_head_str, + .physical_size = om_noop_head_i32x2, + .mode = om_head_mode, + .enabled = om_noop_head_i32, + .current_mode = om_head_current_mode, + .position = om_noop_head_i32x2, + .transform = om_noop_head_i32, + .scale = om_noop_head_i32, + .finished = om_head_finished, + .make = om_noop_head_str, + .model = om_noop_head_str, + .serial_number = om_noop_head_str, + .adaptive_sync = om_noop_head_u32, +}; + +static void om_mode_size_ev(void *data, struct zwlr_output_mode_v1 *mode, + int32_t w, int32_t h) { + (void)data; + for (int i = 0; i < om_nmodes; i++) + if (om_modes[i].res == mode) { + om_modes[i].w = w; + om_modes[i].h = h; + return; + } +} + +static void om_noop_mode_i32(void *data, struct zwlr_output_mode_v1 *mode, + int32_t v) { + (void)data; (void)mode; (void)v; +} + +static void om_noop_mode_void(void *data, struct zwlr_output_mode_v1 *mode) { + (void)data; (void)mode; +} + +static const struct zwlr_output_mode_v1_listener om_mode_listener = { + .size = om_mode_size_ev, + .refresh = om_noop_mode_i32, + .preferred = om_noop_mode_void, + .finished = om_noop_mode_void, +}; + +static void om_mgr_done(void *data, struct zwlr_output_manager_v1 *mgr, + uint32_t serial) { + (void)data; (void)mgr; + if (output_mgmt_want_size && !om_applied && om_head) { + int w = 0, h = 0; + if (om_mode_size(om_current, &w, &h) == 0 && + (w != output_mgmt_w || h != output_mgmt_h)) { + om_report_head("before"); + struct zwlr_output_configuration_v1 *cfg = + zwlr_output_manager_v1_create_configuration(output_mgr, + serial); + zwlr_output_configuration_v1_add_listener(cfg, + &om_config_listener, NULL); + struct zwlr_output_configuration_head_v1 *chead = + zwlr_output_configuration_v1_enable_head(cfg, om_head); + zwlr_output_configuration_head_v1_set_custom_mode( + chead, output_mgmt_w, output_mgmt_h, 0); + zwlr_output_configuration_v1_apply(cfg); + wl_display_flush(display); + return; + } + } + om_report_head(om_applied ? "after" : "head"); + exit(0); +} + +static void om_mgr_finished(void *data, + struct zwlr_output_manager_v1 *mgr) { + (void)data; (void)mgr; +} + +static const struct zwlr_output_manager_v1_listener om_mgr_listener = { + .head = om_mgr_head, + .done = om_mgr_done, + .finished = om_mgr_finished, +}; + +static void output_mgmt_run(void) { + display = wl_display_connect(NULL); + if (!display) { + fprintf(stderr, "wltest: output-mgmt: cannot connect\n"); + exit(1); + } + struct wl_registry *reg = wl_display_get_registry(display); + wl_registry_add_listener(reg, ®istry_listener, NULL); + wl_display_roundtrip(display); + if (!output_mgr) { + fprintf(stderr, "wltest: output-mgmt: manager missing\n"); + exit(1); + } + zwlr_output_manager_v1_add_listener(output_mgr, &om_mgr_listener, NULL); + wl_display_roundtrip(display); + wl_display_roundtrip(display); + for (;;) { + if (wl_display_dispatch(display) < 0) + break; + } +} + /* --- Toplevel icon --- --icon <#RRGGBB> [SIZE]: upload a SIZE×SIZE (default 8) solid-color - icon buffer and set it on the toplevel via xdg-toplevel-icon-v1. */ -static uint8_t ICON_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff }; /* RGBA */ + icon buffer and set it on the toplevel via xdg-toplevel-icon-v1. */static uint8_t ICON_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff }; /* RGBA */ static int icon_size; static bool want_icon; @@ -2464,6 +2716,8 @@ static void print_usage(const char *argv0) { "[--dialog-modal] [--extra-modal] [--extra-delay ] " "[--tearing ] [--fractional-out ] " "[--icon <#RRGGBB> [SIZE]] " + "[--output-mgmt [--output-mgmt-out ] " + "[--output-mgmt-size x]] " "#RRGGBB [WIDTHxHEIGHT]\n" " %s shows a solid-color xdg_toplevel, e.g. #ff0000\n" " --extra also opens a second toplevel from the same client\n" @@ -2505,7 +2759,10 @@ static void print_usage(const char *argv0) { " on the toplevel; --fractional-out writes the\n" " compositor's preferred_scale and exits\n" " --icon uploads a SIZE×SIZE (default 8) solid-color\n" - " icon and sets it via xdg-toplevel-icon-v1", + " icon and sets it via xdg-toplevel-icon-v1\n" + " --output-mgmt lists the output head (name + size)\n" + " to --output-mgmt-out, applying --output-mgmt-size\n" + " first when given", argv0, argv0); } @@ -2576,6 +2833,30 @@ int main(int argc, char **argv) { } fractional_out = argv[i + 1]; i += 2; + } else if (strcmp(argv[i], "--output-mgmt") == 0) { + output_mgmt_mode = true; + i += 1; + } else if (strcmp(argv[i], "--output-mgmt-out") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "wltest: --output-mgmt-out needs a path\n"); + return 1; + } + output_mgmt_out = argv[i + 1]; + i += 2; + } else if (strcmp(argv[i], "--output-mgmt-size") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "wltest: --output-mgmt-size needs WxH\n"); + return 1; + } + if (sscanf(argv[i + 1], "%dx%d", &output_mgmt_w, + &output_mgmt_h) != 2 || + output_mgmt_w <= 0 || output_mgmt_h <= 0) { + fprintf(stderr, "wltest: bad --output-mgmt-size '%s'\n", + argv[i + 1]); + return 1; + } + output_mgmt_want_size = true; + i += 2; } else if (strcmp(argv[i], "--icon") == 0) { if (i + 1 >= argc) { fprintf(stderr, "wltest: --icon needs a color\n"); @@ -2981,6 +3262,12 @@ int main(int argc, char **argv) { gamma_run(); return 0; } + /* Output-management mode: no color/size argument. List the head + and optionally apply a custom size, then exit. */ + if (output_mgmt_mode) { + output_mgmt_run(); + return 0; + } /* Shortcuts-inhibit mode: act as a game window that grabs the compositor's shortcuts. Creates its own focused window. */ if (inhibit_mode) { diff --git a/wlr-protocols/wlr-output-management-unstable-v1.xml b/wlr-protocols/wlr-output-management-unstable-v1.xml new file mode 100644 index 0000000..411e2f0 --- /dev/null +++ b/wlr-protocols/wlr-output-management-unstable-v1.xml @@ -0,0 +1,601 @@ + + + + Copyright © 2019 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol exposes interfaces to obtain and modify output device + configuration. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows reading and writing the current + output device configuration. + + Output devices that display pixels (e.g. a physical monitor or a virtual + output in a window) are represented as heads. Heads cannot be created nor + destroyed by the client, but they can be enabled or disabled and their + properties can be changed. Each head may have one or more available modes. + + Whenever a head appears (e.g. a monitor is plugged in), it will be + advertised via the head event. Immediately after the output manager is + bound, all current heads are advertised. + + Whenever a head's properties change, the relevant wlr_output_head events + will be sent. Not all head properties will be sent: only properties that + have changed need to. + + Whenever a head disappears (e.g. a monitor is unplugged), a + wlr_output_head.finished event will be sent. + + After one or more heads appear, change or disappear, the done event will + be sent. It carries a serial which can be used in a create_configuration + request to update heads properties. + + The information obtained from this protocol should only be used for output + configuration purposes. This protocol is not designed to be a generic + output property advertisement protocol for regular clients. Instead, + protocols such as xdg-output should be used. + + + + + This event introduces a new head. This happens whenever a new head + appears (e.g. a monitor is plugged in) or after the output manager is + bound. + + + + + + + This event is sent after all information has been sent after binding to + the output manager object and after any subsequent changes. This applies + to child head and mode objects as well. In other words, this event is + sent whenever a head or mode is created or destroyed and whenever one of + their properties has been changed. Not all state is re-sent each time + the current configuration changes: only the actual changes are sent. + + This allows changes to the output configuration to be seen as atomic, + even if they happen via multiple events. + + A serial is sent to be used in a future create_configuration request. + + + + + + + Create a new output configuration object. This allows to update head + properties. + + + + + + + + Indicates the client no longer wishes to receive events for output + configuration changes. However the compositor may emit further events, + until the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending manager events. + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + A head is an output device. The difference between a wl_output object and + a head is that heads are advertised even if they are turned off. A head + object only advertises properties and cannot be used directly to change + them. + + A head has some read-only properties: modes, name, description and + physical_size. These cannot be changed by clients. + + Other properties can be updated via a wlr_output_configuration object. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the head name. + + The naming convention is compositor defined, but limited to alphanumeric + characters and dashes (-). Each name is unique among all wlr_output_head + objects, but if a wlr_output_head object is destroyed the same name may + be reused later. The names will also remain consistent across sessions + with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + If the compositor implements the xdg-output protocol and this head is + enabled, the xdg_output.name event must report the same name. + + The name event is sent after a wlr_output_head object is created. This + event is only sent once per object, and the name does not change over + the lifetime of the wlr_output_head object. + + + + + + + This event describes a human-readable description of the head. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. However, do not assume that the name is a reflection of + the make, model, serial of the underlying DRM connector or the display + name of the underlying X11 connection, etc. + + If the compositor implements xdg-output and this head is enabled, + the xdg_output.description must report the same description. + + The description event is sent after a wlr_output_head object is created. + This event is only sent once per object, and the description does not + change over the lifetime of the wlr_output_head object. + + + + + + + This event describes the physical size of the head. This event is only + sent if the head has a physical size (e.g. is not a projector or a + virtual device). + + + + + + + + This event introduces a mode for this head. It is sent once per + supported mode. + + + + + + + This event describes whether the head is enabled. A disabled head is not + mapped to a region of the global compositor space. + + When a head is disabled, some properties (current_mode, position, + transform and scale) are irrelevant. + + + + + + + This event describes the mode currently in use for this head. It is only + sent if the output is enabled. + + + + + + + This events describes the position of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + + This event describes the transformation currently applied to the head. + It is only sent if the output is enabled. + + + + + + + This events describes the scale of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + This event indicates that the head is no longer available. The head + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + + + + + + + + This event describes the manufacturer of the head. + + This must report the same make as the wl_output interface does in its + geometry event. + + Together with the model and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the make of + the head or the definition of a make is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the make string in UI to users. For + that the string provided by the description event should be preferred. + + + + + + + This event describes the model of the head. + + This must report the same model as the wl_output interface does in its + geometry event. + + Together with the make and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the model of + the head or the definition of a model is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the model string in UI to users. For + that the string provided by the description event should be preferred. + + + + + + + This event describes the serial number of the head. + + Together with the make and model events the purpose is to allow clients + to recognize heads from previous sessions and for example load head- + specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the serial + number of the head or the definition of a serial number is not sensible + in the current setup. Clients can still try to identify the head by + available information from other events but should be aware that there + is an increased risk of false positives. + + It is not recommended to display the serial_number string in UI to + users. For that the string provided by the description event should be + preferred. + + + + + + + + + This request indicates that the client will no longer use this head + object. + + + + + + + + + + + + + This event describes whether adaptive sync is currently enabled for + the head or not. Adaptive sync is also known as Variable Refresh + Rate or VRR. + + + + + + + + This object describes an output mode. + + Some heads don't support output modes, in which case modes won't be + advertised. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the mode size. The size is given in physical + hardware units of the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, the output + may be scaled or transformed. + + + + + + + + This event describes the mode's fixed vertical refresh rate. It is only + sent if the mode has a fixed refresh rate. + + + + + + + This event advertises this mode as preferred. + + + + + + This event indicates that the mode is no longer available. The mode + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + + + + + + + + This request indicates that the client will no longer use this mode + object. + + + + + + + This object is used by the client to describe a full output configuration. + + First, the client needs to setup the output configuration. Each head can + be either enabled (and configured) or disabled. It is a protocol error to + send two enable_head or disable_head requests with the same head. It is a + protocol error to omit a head in a configuration. + + Then, the client can apply or test the configuration. The compositor will + then reply with a succeeded, failed or cancelled event. Finally the client + should destroy the configuration object. + + + + + + + + + + + Enable a head. This request creates a head configuration object that can + be used to change the head's properties. + + + + + + + + Disable a head. + + + + + + + Apply the new output configuration. + + In case the configuration is successfully applied, there is no guarantee + that the new output state matches completely the requested + configuration. For instance, a compositor might round the scale if it + doesn't support fractional scaling. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Test the new output configuration. The configuration won't be applied, + but will only be validated. + + Even if the compositor succeeds to test a configuration, applying it may + fail. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Sent after the compositor has successfully applied the changes or + tested them. + + Upon receiving this event, the client should destroy this object. + + If the current configuration has changed, events to describe the changes + will be sent followed by a wlr_output_manager.done event. + + + + + + Sent if the compositor rejects the changes or failed to apply them. The + compositor should revert any changes made by the apply request that + triggered this event. + + Upon receiving this event, the client should destroy this object. + + + + + + Sent if the compositor cancels the configuration because the state of an + output changed and the client has outdated information (e.g. after an + output has been hotplugged). + + The client can create a new configuration with a newer serial and try + again. + + Upon receiving this event, the client should destroy this object. + + + + + + Using this request a client can tell the compositor that it is not going + to use the configuration object anymore. Any changes to the outputs + that have not been applied will be discarded. + + This request also destroys wlr_output_configuration_head objects created + via this object. + + + + + + + This object is used by the client to update a single head's configuration. + + It is a protocol error to set the same property twice. + + + + + + + + + + + + + + This request sets the head's mode. + + + + + + + This request assigns a custom mode to the head. The size is given in + physical hardware units of the output device. If set to zero, the + refresh rate is unspecified. + + It is a protocol error to set both a mode and a custom mode. + + + + + + + + + This request sets the head's position in the global compositor space. + + + + + + + + This request sets the head's transform. + + + + + + + This request sets the head's scale. + + + + + + + + + This request enables/disables adaptive sync. Adaptive sync is also + known as Variable Refresh Rate or VRR. + + + + +