phase 1-2: wl_screen_refresh_heads() enumerates all outputs from wlr_output_layout, populates heads/head_count/primary_head and sets correct scr_width/scr_height from bounding box

This commit is contained in:
lkn 2026-06-06 17:49:10 +02:00
parent 57a8973bc2
commit c5ba4baea0
2 changed files with 570 additions and 28 deletions

View File

@ -0,0 +1,481 @@
# WindowMaker Multi-Monitor Implementation Plan
## Purpose
Implement multi-monitor support in a way that another coding agent can execute with minimal guesswork.
This plan is written for the current `wmaker` tree and assumes:
- X11 and Wayland backends both remain supported
- shared policy code should continue to use the existing `head` abstraction
- the initial goal is **correct multi-monitor geometry and behavior**, not a redesign of workspaces
## Scope
### In scope
- Populate `WScreen.heads[]`, `head_count`, and `primary_head` correctly on both backends
- Make Wayland support multiple outputs instead of only one
- Recompute monitor geometry on output changes
- Keep placement, maximize, fullscreen, icons, dock reservations, and menus head-aware
- Handle output hotplug without leaving windows off-screen
- Preserve existing single-monitor behavior
### Out of scope for phase 1
- Per-monitor workspaces
- Per-monitor dock/clip instances
- Full mixed-DPI polish beyond basic correctness
---
## Existing architecture to preserve
### Shared abstraction
The core already has a backend-neutral monitor/head abstraction.
Relevant files:
- `src/screen.h`
- `src/xinerama.h`
- `src/xinerama.c`
Important fields in `WScreen`:
- `heads`
- `head_count`
- `primary_head`
- `usableArea`
- `totalUsableArea`
Important helper APIs:
- `wGetHeadForRect`
- `wGetHeadForWindow`
- `wGetHeadRelativeToCurrentHead`
- `wGetHeadForPoint`
- `wGetHeadForPointerLocation`
- `wGetRectForHead`
- `wGetUsableAreaForHead`
- `wGetPointToCenterRectInHead`
### Existing backend support
#### X11
- Monitor enumeration already exists in:
- `src/backend/x11/x11_monitor.c`
- Uses Xinerama with RandR screen-change subscriptions
#### Wayland
- Output layout support already exists via wlroots:
- `src/backend/wayland/wl_backend.c`
- `src/backend/wayland/wl_output.c`
- `src/backend/wayland/wl_screen.c`
- Current limitation:
- `wl_screen.c` populates only `heads[0]`
- some Wayland code assumes `usableArea[0]`
---
## Non-goals and policy choices
### Workspace policy
Do **not** implement per-monitor workspaces in this effort.
Use this policy instead:
- a workspace still spans the full virtual desktop
- placement/maximize/focus/icon logic remains **head-aware within that workspace**
This keeps the patch set tractable and aligns with the current helper model.
### Dock / Clip policy
For now:
- keep one logical Dock and one logical Clip model
- make their geometry reservations and placement logic head-aware
Do not introduce per-head docks in this plan.
---
## Canonical monitor contract
Before changing behavior, normalize the meaning of `heads[]`.
### Required invariants
1. `scr->heads` contains monitor rectangles in **global root coordinates**.
2. `scr->head_count >= 1` after screen initialization succeeds.
3. `scr->primary_head` is always a valid index when `head_count > 0`.
4. `scr->scr_width` / `scr->scr_height` represent the bounding box of the full virtual desktop.
5. `wGetRectForHead(scr, i)` must always return a valid monitor rectangle for `0 <= i < head_count`.
6. Single-monitor mode should still behave correctly.
### Preferred normalization
Use **one explicit head even in single-monitor mode**.
That means avoiding the old pattern where `head_count == 0` implies fallback-to-single-head behavior.
If changing all callers at once is too risky, do this in two steps:
1. preserve compatibility in helpers
2. gradually move callers to assume `head_count >= 1`
---
## Execution plan
## Phase 1 — Audit and normalize head initialization
### Goal
Make head data reliable and backend-neutral before extending Wayland.
### Files
- `src/screen.h`
- `src/xinerama.h`
- `src/xinerama.c`
- `src/screen.c`
- `src/backend/x11/x11_monitor.c`
### Tasks
1. Audit all places that read:
- `scr->heads`
- `scr->head_count`
- `scr->primary_head`
2. Decide whether to enforce `head_count >= 1` immediately or via compatibility layer.
3. Ensure the X11 path populates a valid single fallback head when Xinerama is inactive.
4. Ensure head arrays are allocated/freed consistently.
5. Add comments documenting the head contract.
### Acceptance criteria
- X11 single-monitor behavior is unchanged.
- There is a clear contract for `head_count`, `heads`, and `primary_head`.
- Helper functions behave correctly with one or many heads.
---
## Phase 2 — Implement Wayland monitor enumeration
### Goal
Populate all heads from `wlr_output_layout`, not just one.
### Files
- `src/backend/wayland/wl_screen.c`
- `src/backend/wayland/wl_output.c`
- `src/backend/wayland/wl_backend.c`
- `src/backend/backend_types.h`
### Tasks
1. Audit the Wayland global state for available outputs and layout access.
2. In `wl_screen_open()` or the appropriate geometry refresh path:
- enumerate all active outputs
- query each output's box via `wlr_output_layout_get_box`
- build a `WHeadGeometry[]`
3. Set:
- `scr->heads`
- `scr->head_count`
- `scr->primary_head`
4. Set `scr->scr_width` and `scr->scr_height` from the **union of all head rectangles**, not from a single output.
5. Preserve deterministic ordering of heads.
- Recommended: sort by `(x, y)` or preserve wlroots layout iteration order if stable.
6. Choose primary head policy.
- Recommended initial policy: first active output or currently selected backend primary output.
### Acceptance criteria
- On Wayland with two outputs, `scr->head_count == 2`.
- `wGetRectForHead()` returns both outputs with correct geometry.
- `scr->scr_width` / `scr->scr_height` reflect the virtual desktop bounds.
### Notes for the implementing agent
- Keep this backend-only.
- Do not add special placement logic here.
- Just make monitor geometry correct and complete.
---
## Phase 3 — Add dynamic output refresh / hotplug support
### Goal
Rebuild monitor geometry whenever outputs change.
### Files
- `src/backend/wayland/wl_output.c`
- `src/backend/wayland/wl_screen.c`
- `src/screen.c`
- optionally `src/window.c`, `src/dock.c`
### Tasks
1. Identify Wayland output lifecycle hooks:
- new output
- destroyed/removed output
- layout or mode change
2. Add a shared helper to rebuild screen head data.
- Example helper name: `wl_screen_refresh_heads(scr)`
3. Recompute after each relevant event:
- heads
- virtual bounds
- usable areas
4. If an output disappears, repair windows/icons that now lie outside all heads.
### Suggested repair policy
For each affected window:
1. find nearest surviving head
2. preserve relative position when possible
3. clamp inside that head if necessary
4. fall back to centering in `primary_head`
For icons/appicons:
- do the same, but prefer the head they touched before removal
### Acceptance criteria
- Plugging/unplugging outputs does not leave windows permanently off-screen.
- Head helpers reflect the new output topology immediately.
---
## Phase 4 — Make usable-area and reservations per-head
### Goal
Ensure placement/maximize respects per-head reserved areas.
### Files
- `src/xinerama.c`
- `src/screen.c`
- `src/dock.c`
- `src/backend/wayland/wl_dock.c`
### Known problem to fix
Wayland dock code currently assumes `scr->usableArea[0]` in places.
### Tasks
1. Audit all reads/writes of:
- `scr->usableArea`
- `scr->totalUsableArea`
- `scr->reservedAreas`
2. Ensure these arrays are sized for all heads.
3. Ensure dock/clip reservations affect the correct head.
4. When monitor geometry changes, recompute all per-head usable areas.
5. Remove hardcoded `[0]` assumptions.
### Acceptance criteria
- Docks and clips reserve space only on the relevant head.
- Maximize avoids reserved space on the current head.
- Single-monitor behavior remains unchanged.
---
## Phase 5 — Audit all head-aware placement and movement paths
### Goal
Make all geometry-sensitive actions consistent across monitors.
### Files to audit
- `src/placement.c`
- `src/window.c`
- `src/actions.c`
- `src/cycling.c`
- `src/rootmenu.c`
- `src/dialog.c`
- `src/dock.c`
### Tasks
Audit and fix the following behaviors:
#### New window placement
- New windows should open on:
- the pointer head, or
- the owner/transient head, or
- the current head policy already used by helpers
#### Restore/session placement
- Restored geometry should be clamped or translated to a valid head.
#### Maximize/fullscreen
- Maximize should apply to the window's current head unless multi-head fullscreen is explicitly requested.
- Existing fullscreen-monitor helpers in `actions.c` should keep working.
#### Menus/dialogs/transients
- Menus should open on the head that contains the invocation point.
- Transients should prefer the owner head.
- Dialog centering should use the correct head.
#### Icons / miniwindows
- Icon placement should remain on the window's head.
- Auto-arrange should consider all heads, not just head 0.
### Acceptance criteria
- Placement/maximize/fullscreen/menus/transients/icons all behave correctly on two monitors.
---
## Phase 6 — Add explicit monitor navigation commands
### Goal
Expose useful multi-monitor actions to users.
### Files
- `src/actions.c`
- `src/defaults.c`
- `WPrefs.app/KeyboardShortcuts.c`
- possibly menu/defaults docs
### Tasks
1. Audit existing actions using:
- `wGetHeadRelativeToCurrentHead`
- `wGetHeadForWindow`
2. Finish or add commands such as:
- move window to next monitor
- move window to previous monitor
- move focus to next monitor
- maximize on current monitor
3. Expose them through defaults and WPrefs if not already exposed.
### Acceptance criteria
- A user can move windows between monitors with shortcuts.
- The actions behave correctly on X11 and Wayland.
---
## Phase 7 — Mixed scale / DPI follow-up (optional but recommended)
### Goal
Avoid rendering problems on mixed-scale Wayland outputs.
### Files
- `src/backend/wayland/wl_output.c`
- `src/backend/wayland/wl_screen.c`
- WINGs font/render paths
### Tasks
1. Track output scale per output/head.
2. Set output scale in wlroots where appropriate.
3. Feed DPI/scale into font/layout code.
4. Validate titlebars, menus, icons, screenshots, and WINGs rendering.
### Acceptance criteria
- Multi-output works correctly even when outputs have different scales.
---
## Concrete code audit checklist
Use this as a literal checklist while implementing.
### Shared monitor/head code
- [ ] `src/xinerama.c`
- [ ] `src/xinerama.h`
- [ ] `src/screen.c`
- [ ] `src/screen.h`
### X11 backend
- [ ] `src/backend/x11/x11_monitor.c`
### Wayland backend
- [ ] `src/backend/wayland/wl_backend.c`
- [ ] `src/backend/wayland/wl_output.c`
- [ ] `src/backend/wayland/wl_screen.c`
- [ ] `src/backend/wayland/wl_dock.c`
### Placement/window policy
- [ ] `src/placement.c`
- [ ] `src/window.c`
- [ ] `src/actions.c`
- [ ] `src/cycling.c`
- [ ] `src/rootmenu.c`
- [ ] `src/dialog.c`
- [ ] `src/dock.c`
### User-facing configuration
- [ ] `src/defaults.c`
- [ ] `WPrefs.app/KeyboardShortcuts.c`
- [ ] docs / release notes
---
## Suggested implementation order for an agent
If working incrementally, use this order:
1. **Normalize head contract**
2. **Enumerate all Wayland outputs into `heads[]`**
3. **Set virtual desktop bounds correctly**
4. **Recompute heads on hotplug / output change**
5. **Fix per-head usable area and dock reservations**
6. **Audit maximize/placement/fullscreen**
7. **Audit menus/transients/icons**
8. **Add monitor-navigation actions**
9. **Do scale/DPI follow-up**
Do not start with user-facing shortcuts or DPI.
Start with geometry correctness.
---
## Test matrix
## X11 tests
- [ ] single monitor
- [ ] dual monitor horizontal
- [ ] dual monitor vertical
- [ ] mixed resolutions
- [ ] RandR hotplug/unplug
- [ ] maximize on each head
- [ ] fullscreen on each head
- [ ] menu placement
- [ ] dialog placement
- [ ] icon placement
## Wayland tests
Use nested mode if needed.
Suggested setup:
- `WMAKER_WAYLAND_NESTED=1`
- adjust outputs with `wlr-randr`
Run:
- [ ] single output
- [ ] two outputs horizontal
- [ ] two outputs vertical
- [ ] asymmetric offset layout
- [ ] output removal with windows present
- [ ] output addition after startup
- [ ] maximize/fullscreen correctness
- [ ] dock/clip reservation correctness
- [ ] pointer-head placement correctness
- [ ] WINGs app placement correctness
- [ ] XWayland client placement correctness
---
## Debugging guidance for the implementing agent
When behavior is wrong, log these values first:
- `scr->head_count`
- all `scr->heads[i]`
- `scr->primary_head`
- `scr->scr_width`, `scr->scr_height`
- result of `wGetHeadForWindow()` for the affected window
- result of `wGetHeadForPointerLocation()` during placement
- per-head `usableArea` and `totalUsableArea`
For Wayland output changes, log:
- output add/remove
- output layout box for each output
- geometry refresh entry/exit
- window migration decisions on output removal
---
## Definition of done
This effort is complete when all of the following are true:
- `heads[]` is correct on X11 and Wayland
- Wayland supports more than one output
- output hotplug updates geometry correctly
- windows never remain stranded off-screen after output removal
- maximize/fullscreen/placement/icons/menus are head-aware
- dock/clip reservations are per-head
- single-monitor behavior is unchanged
---
## Minimal deliverable if time is limited
If this must be split across multiple PRs, the **minimum useful first PR** is:
1. normalize monitor contract
2. populate all Wayland heads
3. set correct virtual desktop bounds
4. fix obvious `usableArea[0]` assumptions
That alone provides the base needed for all follow-up work.

View File

@ -1,6 +1,8 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <string.h>
#include <limits.h>
#include <X11/Xlib.h>
#include <wayland-server-core.h>
#include <wlr/types/wlr_output.h>
@ -26,17 +28,87 @@ extern void fb_attach_scene_buf_listener(struct wl_frame_buf *fb);
void *W_RegisterBacking(unsigned long id, void *img);
#define MAX_WL_HEADS 16
/* Rebuild scr->heads from the wlr_output_layout. Called during screen
* init and on output hotplug. */
static void
wl_screen_refresh_heads(WScreen *scr)
{
/* Free old head array if any */
if (scr->heads) {
wfree(scr->heads);
scr->heads = NULL;
}
scr->head_count = 0;
scr->primary_head = 0;
if (!wl_state.output_layout)
return;
/* Count outputs first */
int count = 0;
struct wlr_output_layout_output *lo;
wl_list_for_each(lo, &wl_state.output_layout->outputs, link) {
count++;
}
if (count == 0)
return;
if (count > MAX_WL_HEADS)
count = MAX_WL_HEADS;
scr->heads = wmalloc(sizeof(WHeadGeometry) * count);
scr->head_count = count;
/* Compute bounding box across all heads */
int min_x = INT_MAX, min_y = INT_MAX;
int max_x = 0, max_y = 0;
int idx = 0;
wl_list_for_each(lo, &wl_state.output_layout->outputs, link) {
if (idx >= count) break;
struct wlr_box box = {0};
wlr_output_layout_get_box(wl_state.output_layout,
lo->output, &box);
if (box.width == 0 || box.height == 0) {
box.x = 0; box.y = 0;
box.width = (int)lo->output->width;
box.height = (int)lo->output->height;
}
scr->heads[idx].x = box.x;
scr->heads[idx].y = box.y;
scr->heads[idx].width = box.width;
scr->heads[idx].height = box.height;
if (box.x < min_x) min_x = box.x;
if (box.y < min_y) min_y = box.y;
if (box.x + box.width > max_x) max_x = box.x + box.width;
if (box.y + box.height > max_y) max_y = box.y + box.height;
idx++;
}
scr->scr_width = max_x - min_x;
scr->scr_height = max_y - min_y;
/* Use the first output's geometry for scr_width/scr_height if we
* only got a single output anyway (the bounding box is the same). */
if (count == 1) {
scr->scr_width = scr->heads[0].width;
scr->scr_height = scr->heads[0].height;
}
}
/* ------------------------------------------------------------------ */
/* Screen open/close */
void
wl_screen_open(WScreen *scr)
{
/* Set geometry from the headless output created in display_open(). */
scr->scr_width = wl_state.output ?
(int)wl_state.output->width : 1920;
scr->scr_height = wl_state.output ?
(int)wl_state.output->height : 1080;
wl_screen_refresh_heads(scr);
/* Fallback if no output is available yet */
if (scr->head_count == 0) {
scr->scr_width = 1920;
scr->scr_height = 1080;
}
scr->depth = 24;
scr->root_win = (WNativeWindow)0x0FFFFFFF; /* synthetic root window ID */
scr->colormap = 0;
@ -518,37 +590,26 @@ wl_screen_load_tech_font(WScreen *scr)
void
wl_monitors_query(WScreen *scr, WHeadGeometry **heads_out, int *count_out)
{
(void)scr;
if (!wl_state.output) {
if (!wl_state.output_layout) {
*heads_out = NULL;
*count_out = 0;
return;
}
WHeadGeometry *heads = wmalloc(sizeof(WHeadGeometry));
/* Rebuild head data from current layout */
wl_screen_refresh_heads(scr);
if (wl_state.output_layout) {
struct wlr_box box = {0};
wlr_output_layout_get_box(wl_state.output_layout, wl_state.output, &box);
if (box.width > 0 || box.height > 0) {
heads[0].x = box.x;
heads[0].y = box.y;
heads[0].width = box.width;
heads[0].height = box.height;
} else {
heads[0].x = heads[0].y = 0;
heads[0].width = (int)wl_state.output->width;
heads[0].height = (int)wl_state.output->height;
}
} else {
heads[0].x = heads[0].y = 0;
heads[0].width = (int)wl_state.output->width;
heads[0].height = (int)wl_state.output->height;
if (scr->head_count == 0 || !scr->heads) {
*heads_out = NULL;
*count_out = 0;
return;
}
*heads_out = heads;
*count_out = 1;
/* Return a copy the caller owns */
WHeadGeometry *out = wmalloc(sizeof(WHeadGeometry) * scr->head_count);
memcpy(out, scr->heads, sizeof(WHeadGeometry) * scr->head_count);
*heads_out = out;
*count_out = scr->head_count;
}
void wl_monitors_select_events(WScreen *scr) { (void)scr; }