Add --screen-mode WxH[@Hz] to select the native output mode

The RPi5 projector reports a 4K preferred mode although it is really 1080p;
previously only wlr_output_preferred_mode() was tried.  --screen-mode picks
an exact resolution (highest refresh among matches, or an exact @Hz when a
refresh is given) from the output's mode list and, if the request cannot be
satisfied, prints every reported mode and exits before launching anything.

main(): consumes '--screen-mode <val>' and '--screen-mode=<val>' while
compacting the app list so the flag never becomes a tab launcher; malformed
values exit rc 2 with a usage hint.  handle_new_output(): resolution/@Hz
match with list-and-exit fallback.  On the headless backend the request
sizes the output, which also makes the flag testable.

tests: screen_mode_sets_output_size (dump PPM == 480x320), not consumed as
an app (both tabs boot, no 'starts with -' warning), bad values exit rc 2.
This commit is contained in:
Andrew Hamilton 2026-08-16 20:36:38 +10:00
parent 61c863a08b
commit 6ccb3e8d12
2 changed files with 182 additions and 11 deletions

View file

@ -180,6 +180,11 @@ struct tabbed_server {
struct wl_list ptrdevs;
};
/* Requested output resolution from --screen-mode WxH[@Hz]; 0/0/0 means "let
the output pick its preferred mode". Refresh is stored in mHz. */
static int mode_req_w, mode_req_h;
static int mode_req_refresh;
/* ===================== Host-side (nested) state ===================== */
/* One cached ASCII glyph in the overlay's font atlas texture. */
@ -1666,8 +1671,53 @@ server.output = output;
wlr_output_state_init(&state);
wlr_output_state_set_enabled(&state, true);
struct wlr_output_mode *mode = NULL;
if (server.standalone && !server.standalone_headless)
mode = wlr_output_preferred_mode(output);
if (server.standalone && !server.standalone_headless) {
if (mode_req_w > 0) {
/* --screen-mode WxH[@Hz]: pick an exact-resolution mode, highest
refresh when none was requested. A request the output can't
satisfy is a startup error: print every reported mode and exit
(before any client launches or the socket is advertised). */
struct wlr_output_mode *m;
int best_refresh = 0;
wl_list_for_each(m, &output->modes, link) {
if (m->width != mode_req_w || m->height != mode_req_h)
continue;
if (mode_req_refresh > 0) {
if (m->refresh == mode_req_refresh) {
mode = m;
break;
}
continue;
}
if (m->refresh > best_refresh) {
best_refresh = m->refresh;
mode = m;
}
}
if (!mode) {
fprintf(stderr, "tabbed-wlroots: no %dx%d",
mode_req_w, mode_req_h);
if (mode_req_refresh > 0)
fprintf(stderr, "@%u.%03u", mode_req_refresh / 1000,
mode_req_refresh % 1000);
if (wl_list_empty(&output->modes))
fprintf(stderr, " mode; output reports no modes\n");
else
fprintf(stderr, " mode; available:\n");
wl_list_for_each(m, &output->modes, link) {
fprintf(stderr, " %dx%d@%u.%03u%s%s\n",
m->width, m->height, m->refresh / 1000,
m->refresh % 1000,
m->preferred ? " (preferred)" : "",
(m->width == mode_req_w && m->height == mode_req_h)
? " (requested size)" : "");
}
exit(1);
}
} else {
mode = wlr_output_preferred_mode(output);
}
}
if (mode) {
w = mode->width;
h = mode->height;
@ -4528,6 +4578,37 @@ static bool backends_include(const char *env, const char *name) {
/* ===================== Main ===================== */
/* Parse a --screen-mode value: WxH or WxH@Hz (the Hz is matched at the
mode's mHz precision, so "60" == 60.000). Stores into mode_req_w/h/
refresh; returns false on a malformed value. */
static bool parse_screen_mode(const char *val) {
int w = 0, h = 0;
const char *p = val;
for (; *p >= '0' && *p <= '9'; p++)
w = w * 10 + (*p - '0');
if (*p != 'x')
return false;
p++;
for (; *p >= '0' && *p <= '9'; p++)
h = h * 10 + (*p - '0');
if (w <= 0 || h <= 0)
return false;
unsigned refresh_mhz = 0;
if (*p == '@') {
char *end = NULL;
double hz = strtod(p + 1, &end);
if (end == p + 1 || hz <= 0.0 || *end != '\0')
return false;
refresh_mhz = (unsigned)(hz * 1000.0 + 0.5);
} else if (*p != '\0') {
return false;
}
mode_req_w = w;
mode_req_h = h;
mode_req_refresh = (int)refresh_mhz;
return true;
}
int main(int argc, char *argv[]) {
snprintf(tabbed_socket, sizeof(tabbed_socket), "tabbed-wayland-%d",
(int)getpid());
@ -4550,27 +4631,63 @@ int main(int argc, char *argv[]) {
}
/* Ctrl+Shift+T opens a terminal named by $TERMINAL when set, else the
alacritty default. Every command-line argument is one tab launcher.
Handle all of this up front so a missing command can be reported and
the process can exit before any window is created. */
alacritty default. Every command-line argument is one tab launcher,
except --screen-mode WxH[@Hz], which selects the native output mode and
is consumed here. Handle all of this up front so a missing command (or
a bad request) can be reported and the process can exit before any
window is created. */
{
const char *env_term = getenv("TERMINAL");
if (env_term && env_term[0])
term_cmd = env_term;
}
char **appv = calloc((size_t)argc, sizeof(*appv));
if (!appv) {
fprintf(stderr, "tabbed-wlroots: out of memory\n");
return 1;
}
int napps = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
fprintf(stderr,
"usage: %s [prog...]\n"
"usage: %s [--screen-mode WxH[@Hz]] [prog...]\n"
" Select the native output mode (standalone only): "
"e.g. --screen-mode 1920x1080\n"
" or --screen-mode 1920x1080@60. A request nothing "
"supports prints every mode and exits.\n"
" Each <prog> is one shell command; quote it when it "
"takes args, e.g. \"firefox -no-remote\".\n"
" With no arguments, a single terminal tab opens "
"($TERMINAL, default alacritty).\n"
" -h / --help print this help.\n", argv[0]);
free(appv);
return 0;
}
const char *val = NULL;
if (strcmp(argv[i], "--screen-mode") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "tabbed-wlroots: --screen-mode needs a value, "
"e.g. --screen-mode 1920x1080\n");
free(appv);
return 2;
}
val = argv[++i];
} else if (strncmp(argv[i], "--screen-mode=", 14) == 0) {
val = argv[i] + 14;
}
if (val) {
if (!parse_screen_mode(val)) {
fprintf(stderr, "tabbed-wlroots: bad --screen-mode '%s' "
"(use WxH or WxH@Hz, e.g. 1920x1080 or "
"1920x1080@60)\n", val);
free(appv);
return 2;
}
continue;
}
appv[napps++] = argv[i];
}
int total_apps = argc - 1;
int total_apps = napps;
bool default_tab = false;
if (total_apps == 0) {
/* No arguments: open one terminal tab, as Ctrl+Shift+T would. */
@ -4578,10 +4695,11 @@ int main(int argc, char *argv[]) {
total_apps = 1;
}
for (int i = 0; i < total_apps; i++) {
const char *cmd = default_tab ? term_cmd : argv[i + 1];
const char *cmd = default_tab ? term_cmd : appv[i];
if (!cmd_in_path(cmd)) {
fprintf(stderr, "tabbed-wlroots: '%s' not found in PATH%s\n",
cmd, default_tab ? " (set $TERMINAL)" : "");
free(appv);
return 1;
}
}
@ -4820,8 +4938,10 @@ int main(int argc, char *argv[]) {
outputs of its own, so add ours manually; DRM emits its outputs via
new_output. new_output fires during backend start. */
if (!server.standalone || server.standalone_headless) {
host.width = 1280;
host.height = 800;
int w = server.standalone && mode_req_w > 0 ? mode_req_w : 1280;
int h = server.standalone && mode_req_h > 0 ? mode_req_h : 800;
host.width = w;
host.height = h;
wlr_headless_add_output(server.backend, host.width, host.height);
}
@ -4861,7 +4981,7 @@ int main(int argc, char *argv[]) {
remove_socket_file();
return 1;
}
app->cmd = strdup(default_tab ? term_cmd : argv[i + 1]);
app->cmd = strdup(default_tab ? term_cmd : appv[i]);
app->pid = -1;
server.apps[i] = app;
}

View file

@ -718,6 +718,57 @@ def test_standalone_headless_last_tab_exits_cleanly(standalone_session):
assert rc == 0, f"rc={rc} (139 = SIGSEGV); log tail:\n{s.read_log()[-800:]}"
def test_standalone_headless_screen_mode_sets_output_size():
"""--screen-mode WxH sizes the output; on the headless backend it becomes
the add_output() size, observable through the dumped frame dimensions."""
s = _spawn_standalone({}, "--screen-mode", "480x320", "build/wltest '#ff0000'")
try:
s.wait_app_active(0)
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
w, h, _ = s.ppm_rgb(s.dump_host())
assert (w, h) == (480, 320), \
f"output should honour --screen-mode 480x320, got {w}x{h}"
finally:
s.close()
def test_standalone_headless_screen_mode_not_consumed_as_app():
"""--screen-mode is stripped from argv, so it must not become a tab
launcher (no 'starts with -' warning) while both real apps still boot."""
s = _spawn_standalone({}, "--screen-mode", "640x480",
"build/wltest '#ff0000'", "build/wltest '#0000ff'")
try:
s.wait_any_active()
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
assert "starts with '-'" not in s.read_log(), \
f"--screen-mode leaked into the app list:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_screen_mode_bad_value_exits(tmp_path):
"""A malformed --screen-mode value is a startup error (rc 2) naming the
problem, before any window or backend work."""
env = dict(os.environ)
for case in (["--screen-mode"], ["--screen-mode=abc"], ["--screen-mode", "1280"]):
proc = subprocess.Popen(
["env", "-i",
"PATH=" + env["PATH"],
"HOME=" + env.get("HOME", "/"),
"XDG_RUNTIME_DIR=" + env.get("XDG_RUNTIME_DIR", "/tmp"),
"TABBED_MODE=standalone",
"WLR_BACKENDS=headless",
"stdbuf", "-oL",
BIN] + case,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
out, _ = proc.communicate(timeout=15)
assert proc.returncode == 2, \
f"--screen-mode {case} should exit rc 2; rc={proc.returncode}\n{out}"
assert b"--screen-mode" in out, \
f"error should mention --screen-mode:\n{out}"
def test_standalone_headless_overlay_font_initialized(standalone_session):
"""Standalone builds the overlay glyph atlas with the wlr renderer, so tab
titles are drawn next to their icons. Regressed when overlay_font_init()