Skip to content

refactor(compositor): decompose compositor server implementation#112

Merged
K1ngst0m merged 2 commits intomainfrom
dev/split-compositor-src
Mar 7, 2026
Merged

refactor(compositor): decompose compositor server implementation#112
K1ngst0m merged 2 commits intomainfrom
dev/split-compositor-src

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Mar 7, 2026

  • Replace the all-in-one compositor source with subsystem-oriented files
  • Centralize shared compositor state while reducing file-level coupling
  • Isolate protocol, input, and presentation paths behind clearer boundaries
  • Shrink compositor_server.cpp to facade and orchestration duties

Summary by CodeRabbit

  • Refactor

    • Compositor reorganized into modular subsystems while preserving the public API and external behavior.
  • New Features

    • Headless compositor backend with XDG and XWayland support, Wayland layer‑shell rendering, improved input/focus handling with SDL event forwarding, robust cursor theming with fallback and visibility control, and a presentation/export path to produce retrievable frames.
  • Documentation

    • Added comprehensive design, migration, verification, and implementation handoff materials.

- Replace the all-in-one compositor source with subsystem-oriented files
- Centralize shared compositor state while reducing file-level coupling
- Isolate protocol, input, and presentation paths behind clearer boundaries
- Shrink compositor_server.cpp to facade and orchestration duties
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Refactors the monolithic compositor into modular subsystems (core, input, focus, cursor, layer-shell, xdg, xwayland, present), adds compile-safe state/target/hook headers, moves implementations into new source files, and updates CMake to include the new compilation units. CompositorServer remains the external facade.

Changes

Cohort / File(s) Summary
Architecture & Design Docs
openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml, openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md, openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md, openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md, openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md, openspec/specs/compositor-module-layout/spec.md, openspec/specs/compositor-module-layout/spec.md
Adds spec/design/task/handoff docs that define module boundaries, migration waves, verification, and rollback strategy. Documentation-only.
Build Config
src/compositor/CMakeLists.txt
Adds new compositor compilation units (core, cursor, focus, input, layer_shell, present, xdg, xwayland) to the goggles_compositor static library.
Public API / State Headers
src/compositor/compositor_state.hpp, src/compositor/compositor_targets.hpp, src/compositor/compositor_protocol_hooks.hpp
Introduces CompositorState, InputTarget, hook structs (XWayland/XDG/Layer/Constraint) and public function declarations to form compile-safe seams between modules.
Core Initialization
src/compositor/compositor_core.cpp
Implements headless wlroots initialization, logger bridge, display/event-loop/backend/renderer/allocator setup, output layout, Wayland socket binding, compositor thread lifecycle, and teardown.
Input Handling
src/compositor/compositor_input.cpp
Implements input device setup, SDL→Linux translations, event injection/wake, input queues, and input event processing/dispatch (keyboard, pointer motion/button/axis).
Focus & Surface Management
src/compositor/compositor_focus.cpp
Implements focus, pointer-constraint handling, hit-testing/input-target resolution, surface resize requests, surface snapshots, and new CompositorServer APIs for surfaces and input targeting.
Cursor Subsystem
src/compositor/compositor_cursor.cpp
Adds cursor theme loading/fallback, cursor frame management, visibility toggle API, and render-pass cursor overlay with bounds/hotspot handling.
Layer-shell Handling
src/compositor/compositor_layer_shell.cpp
Adds wlr_layer_shell setup, per-layer surface hooks/listeners, initial configure/map/unmap/destroy flows, and layer-surface rendering iteration.
XDG Shell Handling
src/compositor/compositor_xdg.cpp
Adds xdg_shell v3 setup, toplevel/popup hooks and lifecycle handlers (commit/ack/map/destroy), popup unconstrain logic, and cleanup.
XWayland Integration
src/compositor/compositor_xwayland.cpp
Adds XWayland setup (stderr suppression), per-surface hooks, associate/map_request/commit/destroy handling, and input dispatch preparation for XWayland surfaces.
Presentation & Rendering
src/compositor/compositor_present.cpp
Implements present output initialization, present swapchain, render-to-frame pipeline (root surfaces, xwayland popups, overlays, cursor), presented-frame tracking, DMA-BUF export and sync-fence handling.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as Client
participant SDL as SDL/Event Source
participant Server as CompositorServer
participant State as CompositorState
participant Renderer as Render/Present
participant External as External Consumer

Client->>SDL: user input
SDL->>Server: forward_key / forward_mouse_*
Server->>State: inject_event / request_focus / enqueue
State->>State: process_input_events -> resolve input target
State->>Renderer: render_surface_to_frame (on present)
Renderer->>External: export DMA-BUF / signal fences
External->>State: consume presented frame feedback

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • goggles-dev/Goggles#79: Overlaps XDG/XWayland popup hooks, InputTarget types, and render/input-targeting functions introduced here.
  • goggles-dev/Goggles#75: Overlaps presentation subsystem (render_surface_to_frame, presented-frame tracking, DMA-BUF/sync handling).
  • goggles-dev/Goggles#101: Related cursor loading/handling (xcursor theme, fallback cursor, cursor rendering/teardown).

Suggested labels

Review effort 4/5

"🐰 I hop through modular tunnels neat,
splitting code into tidy, rhythmic beats.
Cursors twirl and surfaces align,
one facade keeps the outward sign.
Happy hops — refactor tastes like sweet root-treat!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main refactoring goal: decomposing the compositor server implementation into modular subsystems. It is concise, specific, and directly reflects the core change evident in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev/split-compositor-src

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (6)
src/compositor/compositor_core.cpp (1)

128-131: Guard against multiple wlroots logging initialization.

initialize_wlroots_logging() is called from setup_base_components(). If setup fails and is retried, wlr_log_init would be called again. Consider adding a static flag to ensure one-time initialization.

♻️ Suggested improvement
 auto initialize_wlroots_logging() -> void {
+    static bool initialized = false;
+    if (initialized) {
+        return;
+    }
+    initialized = true;
     const auto level = goggles::get_logger()->level();
     wlr_log_init(wlr_importance_from_log_level(level), wlr_log_bridge);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_core.cpp` around lines 128 - 131, The
initialize_wlroots_logging() function should guard against repeated calls to
wlr_log_init; add a static bool (or std::once_flag) inside
initialize_wlroots_logging() to record if wlroots logging was already
initialized and return early if so, ensuring only the first call (from
setup_base_components() or any retry) invokes wlr_log_init; reference the
existing functions/idents initialize_wlroots_logging, setup_base_components, and
the call to wlr_log_init/wlr_log_bridge when implementing the guard.
src/compositor/compositor_input.cpp (3)

22-28: Duplicate get_time_msec helper.

This function is duplicated in compositor_cursor.cpp. Consider extracting to a shared utility header.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_input.cpp` around lines 22 - 28, Duplicate helper
get_time_msec should be extracted into a shared utility header (e.g.,
time_utils.h) and the duplicate definitions removed: create a single
implementation (preferably static inline or non-ODR-violating function) named
get_time_msec in the new header, add include guards, include that header from
both compositor_input.cpp and compositor_cursor.cpp, and remove the
anonymous-namespace copies so both compilation units use the shared symbol.

411-456: Duplicated cursor reset logic in button and axis handlers.

Lines 422-424 and 444-446 contain identical cursor reset logic:

if (cursor_surface != cursor_reference || !cursor_initialized) {
    reset_cursor_for_surface(cursor_reference);
}

Consider extracting to a helper method to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_input.cpp` around lines 411 - 456, Both
handle_pointer_button_event and handle_pointer_axis_event duplicate the same
cursor-reset logic; extract that check into a small helper (e.g.,
ensure_cursor_for_surface or update_cursor_reference) that accepts the resolved
cursor_reference (wlr_surface*) and calls
reset_cursor_for_surface(cursor_reference) when (cursor_surface !=
cursor_reference || !cursor_initialized). Replace the duplicated if-block in
CompositorState::handle_pointer_button_event and
CompositorState::handle_pointer_axis_event with a single call to the new helper
to remove duplication and keep behavior identical.

152-164: Add clarifying comments for SDL button index mappings.

Buttons 6, 7, and 8 lack SDL3 named constants—SDL3 only defines constants for buttons 1–5 (SDL_BUTTON_LEFT through SDL_BUTTON_X2). Higher button indices are expected to be handled numerically. Adding comments explaining what these hardware button indices represent (e.g., forward/back navigation) would improve clarity without requiring constant definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_input.cpp` around lines 152 - 164, The mapping for
sdl_button to BTN_* values (symbols: sdl_button, BTN_FORWARD, BTN_BACK,
BTN_TASK, BTN_MISC, GOGGLES_LOG_TRACE) is missing clarifying comments; update
the block in compositor_input.cpp that handles sdl_button == 6/7/8 and
sdl_button > 8 to add short comments explaining that SDL3 only provides named
constants for buttons 1–5 (SDL_BUTTON_LEFT..SDL_BUTTON_X2), that indices 6/7/8
are being mapped to forward/back/task hardware buttons respectively, and that
any index >8 is handled numerically as BTN_MISC + (sdl_button - 8); keep the
code logic unchanged and place the comments immediately above each if-case to
document the hardware meaning and why numeric handling is used.
src/compositor/compositor_layer_shell.cpp (1)

34-71: Duplicate RenderSurfaceContext and render_surface_iterator across modules.

This struct and function are duplicated verbatim in compositor_present.cpp (lines 52-89). Consider extracting to a shared internal header to avoid code duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_layer_shell.cpp` around lines 34 - 71, Extract the
duplicated definitions of RenderSurfaceContext and render_surface_iterator into
a single shared internal header, declare the struct and the function there
(preserving the exact symbol names RenderSurfaceContext and
render_surface_iterator), include that header from both
compositor_layer_shell.cpp and compositor_present.cpp, and remove the duplicate
definitions from those .cpp files; ensure the header has proper include guards
(or `#pragma` once) and the function signature remains compatible with existing
usage so no call sites need changing.
src/compositor/compositor_protocol_hooks.hpp (1)

105-110: Use wl_list_empty() for idiomatic listener state checking.

The current check listener.link.next != nullptr && listener.link.prev != nullptr is unreliable: after wl_list_init(), the pointers point to the node itself (not nullptr), so the check doesn't distinguish between "in a list" and "detached". While safe (remove on a self-linked list is a no-op), the idiomatic wlroots pattern uses wl_list_empty():

♻️ Suggested improvement
 inline void detach_listener(wl_listener& listener) {
-    if (listener.link.next != nullptr && listener.link.prev != nullptr) {
+    if (!wl_list_empty(&listener.link)) {
         wl_list_remove(&listener.link);
     }
     wl_list_init(&listener.link);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_protocol_hooks.hpp` around lines 105 - 110, The
detach_listener function should use the idiomatic wl_list_empty() check instead
of inspecting next/prev pointers directly; update detach_listener to call
wl_list_remove(&listener.link) only when !wl_list_empty(&listener.link), then
always call wl_list_init(&listener.link) to reinitialize the listener; reference
the function detach_listener and the wl_list_empty/wl_list_remove/wl_list_init
calls when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md`:
- Line 40: The list item for "compositor_layer_shell.*" is malformed due to a
doubled list marker; edit the proposal markdown and fix the bullet so it uses a
single list marker (remove the extra "-" at the start) so the
`compositor_layer_shell.*` entry becomes a normal list item and renders within
the surrounding boundary list.

In
`@openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md`:
- Around line 103-108: The table rows include unescaped pipe characters inside
code spans (e.g. the regexes
"goggles_tests|goggles_test_child_death_signal|goggles_test_headless_child_exit",
"goggles_auto_input_forwarding_(x11|wayland)", and
"goggles_headless_integration(_png_exists)?") which break the Markdown table;
fix by escaping each '|' inside those table cells (use '\|' or replace with
'|') or wrap the entire command in an HTML code element that uses '|'
for pipes so the cell remains a single column and the traceability matrix
renders correctly.
- Line 89: Update the broken archived references in the checklist: locate the
checklist items that currently reference
openspec/changes/refactor-compositor-server-modules/... (the lines containing
"8.1 Confirm implementation matches
`openspec/changes/refactor-compositor-server-modules/specs/compositor-module-layout/spec.md`
and `openspec/changes/refactor-compositor-server-modules/design.md`" and the
similar entry at line 91) and change those paths to the new dated archive
location
(openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/...) so
both links point to the archived spec and design files instead of the old
non-archived paths.

In `@openspec/specs/compositor-module-layout/spec.md`:
- Around line 3-4: Replace the placeholder "TBD" under the "## Purpose" heading
in spec.md with a concrete, final purpose statement describing the intent and
scope of the compositor-module-layout spec (e.g., what the module layout
standardizes, who it affects, and any high-level goals). Update the "## Purpose"
section text (the block currently containing "TBD - created by archiving change
refactor-compositor-server-modules. Update Purpose after archive.") to a
complete sentence or short paragraph that serves as the canonical description
for the published spec.

In `@src/compositor/compositor_core.cpp`:
- Around line 212-237: CompositorState::setup_event_loop_fd currently creates a
new eventfd unconditionally which can overwrite and leak the existing event_fd;
before creating a new fd, check if event_fd already holds a valid descriptor
(via util::UniqueFd's validity check or event_fd.get() >= 0) and if so either
return success immediately or close/remove the existing fd and its
wl_event_source (event_source) before creating a new one; ensure you also avoid
adding a duplicate wl_event_loop_add_fd by checking/clearing event_source, and
only call wl_event_loop_add_fd when no valid event_fd/event_source exists.

In `@src/compositor/compositor_focus.cpp`:
- Around line 59-62: get_surfaces() currently calls
m_impl->state.get_surfaces_snapshot() directly from the caller thread, causing a
data race because the compositor thread mutates focused_surface,
focused_xsurface and hook fields under a different synchronization boundary; fix
by routing the snapshot request to the compositor thread (e.g. post a task to
the compositor's event loop and return the snapshot from that task) or ensure
both readers and writers use the same mutex by taking hooks_mutex (or the
state’s single mutex) for both get_surfaces() and all compositor-side mutations;
update get_surfaces(), get_surfaces_snapshot(), and any compositor mutation
sites that touch focused_surface/focused_xsurface/hooks to use the chosen
synchronization approach so reads and writes share a single boundary.
- Around line 241-245: Previous focused toplevels aren't being deactivated
before being overwritten, causing multiple clients to remain active; before
assigning the new targets in the focus-switch paths (the blocks that set
focused_xsurface and focused_surface and call deactivate_constraint()),
explicitly call the appropriate deactivate function on the currently stored
focus (e.g., if focused_xsurface != nullptr call its
deactivate/toplevel_deactivate method, or if focused_surface != nullptr call its
deactivate method) then clear them and proceed to activate the incoming surface;
apply the same fix to the other focus-switch locations referenced (around the
blocks at 281-290 and 344-346) so the old toplevel always receives a deactivate
call prior to being replaced.

In `@src/compositor/compositor_input.cpp`:
- Around line 271-294: The two separate pointer_axis injections in
CompositorServer::forward_mouse_wheel can enqueue only one axis when the queue
is near capacity, causing inconsistent diagonal scrolls; change this to emit a
single consolidated event (e.g., add a new InputEventType::pointer_axis2 or
extend InputEvent with axis_x and axis_y fields) and populate both vertical (y)
and horizontal (x) values in one InputEvent before calling inject_event once, or
alternatively modify inject_event to support an atomic batch enqueue so both
axis values are accepted together; update any handling code that reads
InputEvent to consume the new combined event representation accordingly
(references: CompositorServer::forward_mouse_wheel, inject_event, InputEvent,
InputEventType::pointer_axis).

In `@src/compositor/compositor_layer_shell.cpp`:
- Around line 234-245: The unmap handler
CompositorState::handle_layer_surface_unmap uses keyboard without checking for
null; update the conditional that currently checks seat, hooks->surface, and
focused_surface to also verify keyboard is non-null (e.g., keyboard != nullptr)
before calling wlr_seat_set_keyboard(seat, keyboard.get()) and
wlr_seat_keyboard_notify_enter(..., keyboard->keycodes, keyboard->num_keycodes,
&keyboard->modifiers); ensure the early-return or conditional scope prevents
dereferencing keyboard when it's null.
- Around line 253-258: The destroy handler is missing a null check for keyboard
before dereferencing it; update the handler to ensure keyboard is non-null (in
addition to existing checks for seat, hooks->surface, focused_surface) before
calling wlr_seat_set_keyboard and wlr_seat_keyboard_notify_enter and before
accessing keyboard->keycodes, keyboard->num_keycodes, or &keyboard->modifiers so
you don’t dereference a null pointer (i.e., mirror the null-check logic used
here for keyboard when handling destruction in compositor_layer_shell.cpp).
- Around line 221-232: In CompositorState::handle_layer_surface_map, add a null
check for the keyboard pointer before calling wlr_seat_set_keyboard and
wlr_seat_keyboard_notify_enter: verify keyboard is non-null (e.g., if (keyboard)
or if (keyboard.get())) and only then call wlr_seat_set_keyboard(seat,
keyboard.get()) and wlr_seat_keyboard_notify_enter(seat, hooks->surface,
keyboard->keycodes, keyboard->num_keycodes, &keyboard->modifiers); otherwise
skip those calls so no dereference of keyboard occurs; keep LayerSurfaceHooks
handling and request_present_reset() unchanged.

In `@src/compositor/compositor_present.cpp`:
- Around line 93-139: The initialize_present_output() function can leak the
previous swapchain when called multiple times; before creating a new
present_swapchain in CompositorState::initialize_present_output(), check if
present_swapchain is non-null and call wlr_swapchain_destroy(present_swapchain)
(then set present_swapchain = nullptr) to free the old resource, ensuring you
clear any dependent state (e.g., present_width/present_height if needed) before
creating the new swapchain.

In `@src/compositor/compositor_xdg.cpp`:
- Around line 234-260: The code erases the unique_ptr owner in xdg_popup_hooks
(inside the scoped_lock using hooks_mutex) while still dereferencing hooks
afterwards to compare hooks->surface against keyboard_entered_surface and
pointer_entered_surface; move those dereferences before destroying the owner or
stash the needed data first: capture hooks->surface into a local variable (e.g.
auto* surface = hooks->surface) or perform the
keyboard_entered_surface/pointer_entered_surface comparisons and resets prior to
the block that finds/erases the XdgPopupHooks unique_ptr (functions/vars
involved: XdgPopupHooks, xdg_popup_hooks, hooks, hooks_mutex,
keyboard_entered_surface, pointer_entered_surface, detach_listener).

In `@src/compositor/compositor_xwayland.cpp`:
- Around line 223-275: The code currently caches XWayland metadata
(title/class_name) only during the associate handler in XWaylandSurfaceHooks so
later renames never propagate; either update those cached fields whenever the
XWayland surface metadata changes (add a listener in XWaylandSurfaceHooks for
metadata/title/class change and refresh the cached fields there, e.g., in the
associate lambda or a new metadata notify) or change get_surfaces_snapshot() to
read h->xsurface->title and h->xsurface->class_name live instead of using the
cached copies—modify the associate/destroy/commit-related handlers
(XWaylandSurfaceHooks::associate, ::destroy, and the commit notify) or the
get_surfaces_snapshot implementation to ensure title/class_name are kept
current.

---

Nitpick comments:
In `@src/compositor/compositor_core.cpp`:
- Around line 128-131: The initialize_wlroots_logging() function should guard
against repeated calls to wlr_log_init; add a static bool (or std::once_flag)
inside initialize_wlroots_logging() to record if wlroots logging was already
initialized and return early if so, ensuring only the first call (from
setup_base_components() or any retry) invokes wlr_log_init; reference the
existing functions/idents initialize_wlroots_logging, setup_base_components, and
the call to wlr_log_init/wlr_log_bridge when implementing the guard.

In `@src/compositor/compositor_input.cpp`:
- Around line 22-28: Duplicate helper get_time_msec should be extracted into a
shared utility header (e.g., time_utils.h) and the duplicate definitions
removed: create a single implementation (preferably static inline or
non-ODR-violating function) named get_time_msec in the new header, add include
guards, include that header from both compositor_input.cpp and
compositor_cursor.cpp, and remove the anonymous-namespace copies so both
compilation units use the shared symbol.
- Around line 411-456: Both handle_pointer_button_event and
handle_pointer_axis_event duplicate the same cursor-reset logic; extract that
check into a small helper (e.g., ensure_cursor_for_surface or
update_cursor_reference) that accepts the resolved cursor_reference
(wlr_surface*) and calls reset_cursor_for_surface(cursor_reference) when
(cursor_surface != cursor_reference || !cursor_initialized). Replace the
duplicated if-block in CompositorState::handle_pointer_button_event and
CompositorState::handle_pointer_axis_event with a single call to the new helper
to remove duplication and keep behavior identical.
- Around line 152-164: The mapping for sdl_button to BTN_* values (symbols:
sdl_button, BTN_FORWARD, BTN_BACK, BTN_TASK, BTN_MISC, GOGGLES_LOG_TRACE) is
missing clarifying comments; update the block in compositor_input.cpp that
handles sdl_button == 6/7/8 and sdl_button > 8 to add short comments explaining
that SDL3 only provides named constants for buttons 1–5
(SDL_BUTTON_LEFT..SDL_BUTTON_X2), that indices 6/7/8 are being mapped to
forward/back/task hardware buttons respectively, and that any index >8 is
handled numerically as BTN_MISC + (sdl_button - 8); keep the code logic
unchanged and place the comments immediately above each if-case to document the
hardware meaning and why numeric handling is used.

In `@src/compositor/compositor_layer_shell.cpp`:
- Around line 34-71: Extract the duplicated definitions of RenderSurfaceContext
and render_surface_iterator into a single shared internal header, declare the
struct and the function there (preserving the exact symbol names
RenderSurfaceContext and render_surface_iterator), include that header from both
compositor_layer_shell.cpp and compositor_present.cpp, and remove the duplicate
definitions from those .cpp files; ensure the header has proper include guards
(or `#pragma` once) and the function signature remains compatible with existing
usage so no call sites need changing.

In `@src/compositor/compositor_protocol_hooks.hpp`:
- Around line 105-110: The detach_listener function should use the idiomatic
wl_list_empty() check instead of inspecting next/prev pointers directly; update
detach_listener to call wl_list_remove(&listener.link) only when
!wl_list_empty(&listener.link), then always call wl_list_init(&listener.link) to
reinitialize the listener; reference the function detach_listener and the
wl_list_empty/wl_list_remove/wl_list_init calls when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5570b1b4-d8d0-4cbc-a8cd-23e2668fbe60

📥 Commits

Reviewing files that changed from the base of the PR and between 82c677f and 5426d8b.

📒 Files selected for processing (20)
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/.openspec.yaml
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/design.md
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/implementation-handoff.md
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/proposal.md
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/specs/compositor-module-layout/spec.md
  • openspec/changes/archive/2026-03-07-refactor-compositor-server-modules/tasks.md
  • openspec/specs/compositor-module-layout/spec.md
  • src/compositor/CMakeLists.txt
  • src/compositor/compositor_core.cpp
  • src/compositor/compositor_cursor.cpp
  • src/compositor/compositor_focus.cpp
  • src/compositor/compositor_input.cpp
  • src/compositor/compositor_layer_shell.cpp
  • src/compositor/compositor_present.cpp
  • src/compositor/compositor_protocol_hooks.hpp
  • src/compositor/compositor_server.cpp
  • src/compositor/compositor_state.hpp
  • src/compositor/compositor_targets.hpp
  • src/compositor/compositor_xdg.cpp
  • src/compositor/compositor_xwayland.cpp

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/compositor/compositor_core.cpp (1)

261-277: Consider making output resolution configurable.

The hardcoded 1920x1080 resolution works for headless mode but may limit flexibility. Consider accepting dimensions as parameters or reading from configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_core.cpp` around lines 261 - 277,
CompositorState::setup_output currently hardcodes 1920x1080 when calling
wlr_headless_add_output; change it to accept configurable dimensions (either
parameters on CompositorState::setup_output or loading width/height from the
existing configuration object) and pass those values to wlr_headless_add_output
instead of literals. Update any callers of setup_output (or CompositorState
constructor) to provide or propagate the configured width/height, and validate
ranges before calling wlr_headless_add_output; keep the fallback default to
1920x1080 if no config is provided. Ensure identifiers referenced:
CompositorState::setup_output, output, wlr_headless_add_output, and any config
accessor you add/modify.
src/compositor/compositor_xwayland.cpp (1)

388-393: Minor: Redundant clearing of entered surfaces.

When clear_focus is true, keyboard_entered_surface and pointer_entered_surface are cleared twice: first conditionally at lines 388-393 (if they match surface), then unconditionally at lines 409-410. The second clear is redundant when the first already matched.

♻️ Suggested simplification
     if (keyboard_entered_surface == surface) {
         keyboard_entered_surface = nullptr;
     }
     if (pointer_entered_surface == surface) {
         pointer_entered_surface = nullptr;
     }

     if (presented_surface == surface) {
         clear_presented_frame();
     }

     if (xsurface && xsurface->override_redirect) {
         request_present_reset();
     }

     if (!clear_focus) {
         return;
     }

     GOGGLES_LOG_DEBUG("Focused XWayland surface destroyed: ptr={}", static_cast<void*>(xsurface));
     deactivate_constraint();
-    keyboard_entered_surface = nullptr;
-    pointer_entered_surface = nullptr;
     cursor_surface = nullptr;

Also applies to: 409-410

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_xwayland.cpp` around lines 388 - 393,
keyboard_entered_surface and pointer_entered_surface are being cleared twice
when clear_focus is true; remove the redundant early conditional clears (the
block that does "if (keyboard_entered_surface == surface) {
keyboard_entered_surface = nullptr; }" and the pointer equivalent) or instead
guard that early block with a "!clear_focus" check so the later unconditional
clears (when clear_focus is true) are the only path that clears them; update the
code around the surface handling to either delete the first conditional clear
block or wrap it in if (!clear_focus) to avoid double-clearing of
keyboard_entered_surface and pointer_entered_surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/compositor/compositor_focus.cpp`:
- Around line 727-755: auto_focus_next_surface iterates xwayland_hooks and
xdg_hooks without locking hooks_mutex which risks data races; either acquire
hooks_mutex for the duration of the two iterations (lock before iterating
xwayland_hooks and xdg_hooks and unlock after) to protect access to the
containers, or if the function is guaranteed to run only on the compositor
thread, add a clear comment in CompositorState::auto_focus_next_surface stating
that the compositor thread exclusively owns xwayland_hooks/xdg_hooks and
hooks_mutex need not be taken; apply the change near the start of
auto_focus_next_surface and ensure use of the existing hooks_mutex name and that
calls to focus_xwayland_surface, wlr_xdg_toplevel_set_activated, and
focus_surface remain executed while holding or after documenting the ownership
contract consistently.
- Around line 93-96: Multiple functions read the non-atomic pointers
focused_surface and focused_xsurface without synchronization, causing data
races; update all accesses (reads and writes) to those pointers to be protected
by the existing hooks_mutex (or convert the pointers to std::atomic if you
prefer an atomic approach). Specifically, wrap the early-exit checks in
focus_surface() and focus_xwayland_surface(), the reads in
handle_new_pointer_constraint(), the code paths around the reads in
compositor_focus.cpp (those at lines noted in the review), and the readers in
handle_layer_surface_unmap() and handle_layer_surface_destroy() so they take
hooks_mutex before reading focused_surface/focused_xsurface; also ensure
destructors and shutdown() similarly acquire hooks_mutex or use the atomic
variant to avoid races. Ensure you lock hooks_mutex consistently wherever
focused_surface or focused_xsurface are read or written (or replace those
pointers with atomic pointers and adjust reads/writes accordingly).

---

Nitpick comments:
In `@src/compositor/compositor_core.cpp`:
- Around line 261-277: CompositorState::setup_output currently hardcodes
1920x1080 when calling wlr_headless_add_output; change it to accept configurable
dimensions (either parameters on CompositorState::setup_output or loading
width/height from the existing configuration object) and pass those values to
wlr_headless_add_output instead of literals. Update any callers of setup_output
(or CompositorState constructor) to provide or propagate the configured
width/height, and validate ranges before calling wlr_headless_add_output; keep
the fallback default to 1920x1080 if no config is provided. Ensure identifiers
referenced: CompositorState::setup_output, output, wlr_headless_add_output, and
any config accessor you add/modify.

In `@src/compositor/compositor_xwayland.cpp`:
- Around line 388-393: keyboard_entered_surface and pointer_entered_surface are
being cleared twice when clear_focus is true; remove the redundant early
conditional clears (the block that does "if (keyboard_entered_surface ==
surface) { keyboard_entered_surface = nullptr; }" and the pointer equivalent) or
instead guard that early block with a "!clear_focus" check so the later
unconditional clears (when clear_focus is true) are the only path that clears
them; update the code around the surface handling to either delete the first
conditional clear block or wrap it in if (!clear_focus) to avoid double-clearing
of keyboard_entered_surface and pointer_entered_surface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7879f5b-4a62-4be2-821e-7552bcacb396

📥 Commits

Reviewing files that changed from the base of the PR and between 5426d8b and 19879ac.

📒 Files selected for processing (4)
  • src/compositor/compositor_core.cpp
  • src/compositor/compositor_focus.cpp
  • src/compositor/compositor_xdg.cpp
  • src/compositor/compositor_xwayland.cpp

@K1ngst0m K1ngst0m force-pushed the dev/split-compositor-src branch from 19879ac to 7e247ab Compare March 7, 2026 07:38
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (5)
src/compositor/compositor_layer_shell.cpp (3)

243-248: ⚠️ Potential issue | 🟠 Major

Same null check needed for keyboard in unmap handler.

🐛 Proposed fix
     if (seat && hooks->surface && seat->keyboard_state.focused_surface == hooks->surface &&
-        next_focus_surface) {
+        next_focus_surface && keyboard) {
         wlr_seat_set_keyboard(seat, keyboard.get());
         wlr_seat_keyboard_notify_enter(seat, next_focus_surface, keyboard->keycodes,
                                        keyboard->num_keycodes, &keyboard->modifiers);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_layer_shell.cpp` around lines 243 - 248, In the
unmap handler add a null check for the keyboard before using it: ensure keyboard
is non-null along with seat, hooks->surface, and next_focus_surface before
calling wlr_seat_set_keyboard and wlr_seat_keyboard_notify_enter (the same
pattern as the map handler), i.e. only call wlr_seat_set_keyboard(seat,
keyboard.get()) and wlr_seat_keyboard_notify_enter(...) when keyboard != nullptr
to avoid dereferencing a null keyboard.

265-270: ⚠️ Potential issue | 🟠 Major

Same null check needed for keyboard in destroy handler.

🐛 Proposed fix
     if (seat && hooks->surface && seat->keyboard_state.focused_surface == hooks->surface &&
-        next_focus_surface) {
+        next_focus_surface && keyboard) {
         wlr_seat_set_keyboard(seat, keyboard.get());
         wlr_seat_keyboard_notify_enter(seat, next_focus_surface, keyboard->keycodes,
                                        keyboard->num_keycodes, &keyboard->modifiers);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_layer_shell.cpp` around lines 265 - 270, The
destroy handler uses keyboard without null-checking it; update the conditional
that currently checks seat, hooks->surface, seat->keyboard_state.focused_surface
and next_focus_surface to also require keyboard (e.g., keyboard != nullptr)
before calling wlr_seat_set_keyboard(seat, keyboard.get()) and
wlr_seat_keyboard_notify_enter(... keyboard->keycodes, keyboard->num_keycodes,
&keyboard->modifiers). Ensure the same combined null-check (keyboard) is added
so wlr_seat_set_keyboard and wlr_seat_keyboard_notify_enter are only invoked
when keyboard is valid.

221-232: ⚠️ Potential issue | 🟠 Major

Add null check for keyboard before dereferencing.

The code accesses keyboard->keycodes, keyboard->num_keycodes, and keyboard->modifiers without verifying keyboard is non-null. If input device setup fails or is deferred, this could cause a null pointer dereference.

🐛 Proposed fix
 void CompositorState::handle_layer_surface_map(LayerSurfaceHooks* hooks) {
     hooks->mapped = true;
 
-    if (hooks->layer_surface && hooks->layer_surface->current.keyboard_interactive ==
+    if (keyboard && hooks->layer_surface && hooks->layer_surface->current.keyboard_interactive ==
                                     ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) {
         wlr_seat_set_keyboard(seat, keyboard.get());
         wlr_seat_keyboard_notify_enter(seat, hooks->surface, keyboard->keycodes,
                                        keyboard->num_keycodes, &keyboard->modifiers);
     }
 
     request_present_reset();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_layer_shell.cpp` around lines 221 - 232, In
CompositorState::handle_layer_surface_map, avoid dereferencing keyboard when it
may be null: before calling wlr_seat_set_keyboard and
wlr_seat_keyboard_notify_enter, add a null-check (e.g. if (keyboard) { ... })
and only call wlr_seat_set_keyboard(seat, keyboard.get()) and
wlr_seat_keyboard_notify_enter(seat, hooks->surface, keyboard->keycodes,
keyboard->num_keycodes, &keyboard->modifiers) inside that branch; leave
request_present_reset() and the mapped flag behavior unchanged.
src/compositor/compositor_core.cpp (1)

212-237: ⚠️ Potential issue | 🟡 Minor

Check for existing eventfd before creating a new one.

If setup_event_loop_fd() is called when event_fd already holds a valid descriptor, the old fd would be leaked (overwritten by the new UniqueFd). While this may not happen in practice with the current initialization sequence, adding a guard makes the code more robust.

🐛 Proposed fix
 auto CompositorState::setup_event_loop_fd() -> Result<void> {
     GOGGLES_PROFILE_FUNCTION();
+    if (event_fd.valid()) {
+        return {}; // Already initialized
+    }
     int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_core.cpp` around lines 212 - 237, The
setup_event_loop_fd function can overwrite an existing event_fd and leak the old
descriptor; update CompositorState::setup_event_loop_fd to first check whether
event_fd already holds a valid descriptor (e.g., by testing event_fd.get() or a
valid()/is_valid() helper) and if so either return success immediately or
reuse/close it before creating a new eventfd; ensure you also avoid adding a
second wl_event_loop_add_fd when event_source is already set (check
event_source) and only call wl_event_loop_add_fd when creating a new event_fd so
event_source and event_fd remain consistent.
src/compositor/compositor_xwayland.cpp (1)

291-292: ⚠️ Potential issue | 🟡 Minor

XWayland metadata cached only once during associate.

The hook captures title and class_name once during associate, but get_surfaces_snapshot() later serves those cached strings. Any later rename on the same XWayland window will never reach callers, so the surface list can drift until the window is recreated.

Consider refreshing the cache on metadata changes or reading live fields when building the snapshot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_xwayland.cpp` around lines 291 - 292, The current
associate() stores XWayland metadata into hooks->title and hooks->class_name
once (from xsurface->title / xsurface->class_) so later renames don't propagate
to callers of get_surfaces_snapshot(); instead, either update the cache when
metadata changes or stop using the cached strings: modify
get_surfaces_snapshot() to read the live xsurface->title and xsurface->class_
fields (or register a metadata-change callback in associate() that updates
hooks->title/hooks->class_name when xsurface metadata changes) so snapshot
generation always reflects current window title/class.
🧹 Nitpick comments (3)
src/compositor/compositor_focus.cpp (1)

475-482: Consider making get_surface_extent static or moving to anonymous namespace.

This helper function is defined at file scope without static or being inside the anonymous namespace, which means it has external linkage. Since it appears to be an internal helper, consider moving it inside the anonymous namespace (like get_root_xdg_surface and others below) or marking it static to prevent ODR violations if a similar function exists elsewhere.

♻️ Suggested fix
+namespace {
+
 auto get_surface_extent(wlr_surface* surface) -> std::optional<std::pair<uint32_t, uint32_t>> {
     if (surface && surface->current.width > 0 && surface->current.height > 0) {
         return std::pair<uint32_t, uint32_t>(static_cast<uint32_t>(surface->current.width),
                                              static_cast<uint32_t>(surface->current.height));
     }
 
     return std::nullopt;
 }
+
+} // namespace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_focus.cpp` around lines 475 - 482, The helper
get_surface_extent has external linkage but is only used internally; change its
linkage by either marking the function static (add the static keyword to the
get_surface_extent declaration/definition) or move its definition into the
file's anonymous namespace alongside helpers like get_root_xdg_surface so it has
internal linkage; update any references if you relocate it and ensure the
signature and return type remain unchanged.
src/compositor/compositor_core.cpp (1)

334-336: Consider removing redundant conditional.

The if (keyboard) check before keyboard.reset() is unnecessary since reset() on a null unique_ptr is a no-op. This is a minor style nit.

♻️ Suggested simplification
-    if (keyboard) {
-        keyboard.reset();
-    }
+    keyboard.reset();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_core.cpp` around lines 334 - 336, Remove the
redundant conditional guarding the unique_ptr reset: replace the `if (keyboard)
{ keyboard.reset(); }` pattern with a single unconditional `keyboard.reset();`
call (the `keyboard` unique_ptr already no-ops on null). Locate the usage in
compositor_core.cpp where the `keyboard` unique_ptr is reset (search for the
`keyboard` identifier in the function surrounding the shown diff) and simplify
accordingly.
src/compositor/compositor_xdg.cpp (1)

170-203: Consider reducing lock scope when calling wlroots functions.

The hooks_mutex is acquired at line 179 to protect access to state.xdg_popup_hooks within get_popup_owner_root_surface. However, the lock is held throughout the call to wlr_xdg_popup_unconstrain_from_box at line 192. Since this is an external wlroots library function that modifies popup state, it could potentially emit signals that trigger callbacks. If any such callbacks attempt to acquire hooks_mutex, a deadlock would occur. Consider restructuring to release the lock after gathering the constraint box parameters but before calling unconstrain_from_box.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compositor/compositor_xdg.cpp` around lines 170 - 203, The lock on
hooks_mutex in CompositorState::handle_xdg_popup_commit is held across the
wlroots call wlr_xdg_popup_unconstrain_from_box which can emit signals and cause
deadlocks; change the code to acquire hooks_mutex only to call
get_popup_owner_root_surface and compute a local wlr_box constraint_box (and
copy any needed pointers/flags such as hooks->popup) then release the lock
before calling wlr_xdg_popup_unconstrain_from_box, and only call the wlroots
function when not holding hooks_mutex (ensure any pointers used remain valid
after the lock is released).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/compositor/compositor_core.cpp`:
- Around line 212-237: The setup_event_loop_fd function can overwrite an
existing event_fd and leak the old descriptor; update
CompositorState::setup_event_loop_fd to first check whether event_fd already
holds a valid descriptor (e.g., by testing event_fd.get() or a
valid()/is_valid() helper) and if so either return success immediately or
reuse/close it before creating a new eventfd; ensure you also avoid adding a
second wl_event_loop_add_fd when event_source is already set (check
event_source) and only call wl_event_loop_add_fd when creating a new event_fd so
event_source and event_fd remain consistent.

In `@src/compositor/compositor_layer_shell.cpp`:
- Around line 243-248: In the unmap handler add a null check for the keyboard
before using it: ensure keyboard is non-null along with seat, hooks->surface,
and next_focus_surface before calling wlr_seat_set_keyboard and
wlr_seat_keyboard_notify_enter (the same pattern as the map handler), i.e. only
call wlr_seat_set_keyboard(seat, keyboard.get()) and
wlr_seat_keyboard_notify_enter(...) when keyboard != nullptr to avoid
dereferencing a null keyboard.
- Around line 265-270: The destroy handler uses keyboard without null-checking
it; update the conditional that currently checks seat, hooks->surface,
seat->keyboard_state.focused_surface and next_focus_surface to also require
keyboard (e.g., keyboard != nullptr) before calling wlr_seat_set_keyboard(seat,
keyboard.get()) and wlr_seat_keyboard_notify_enter(... keyboard->keycodes,
keyboard->num_keycodes, &keyboard->modifiers). Ensure the same combined
null-check (keyboard) is added so wlr_seat_set_keyboard and
wlr_seat_keyboard_notify_enter are only invoked when keyboard is valid.
- Around line 221-232: In CompositorState::handle_layer_surface_map, avoid
dereferencing keyboard when it may be null: before calling wlr_seat_set_keyboard
and wlr_seat_keyboard_notify_enter, add a null-check (e.g. if (keyboard) { ...
}) and only call wlr_seat_set_keyboard(seat, keyboard.get()) and
wlr_seat_keyboard_notify_enter(seat, hooks->surface, keyboard->keycodes,
keyboard->num_keycodes, &keyboard->modifiers) inside that branch; leave
request_present_reset() and the mapped flag behavior unchanged.

In `@src/compositor/compositor_xwayland.cpp`:
- Around line 291-292: The current associate() stores XWayland metadata into
hooks->title and hooks->class_name once (from xsurface->title /
xsurface->class_) so later renames don't propagate to callers of
get_surfaces_snapshot(); instead, either update the cache when metadata changes
or stop using the cached strings: modify get_surfaces_snapshot() to read the
live xsurface->title and xsurface->class_ fields (or register a metadata-change
callback in associate() that updates hooks->title/hooks->class_name when
xsurface metadata changes) so snapshot generation always reflects current window
title/class.

---

Nitpick comments:
In `@src/compositor/compositor_core.cpp`:
- Around line 334-336: Remove the redundant conditional guarding the unique_ptr
reset: replace the `if (keyboard) { keyboard.reset(); }` pattern with a single
unconditional `keyboard.reset();` call (the `keyboard` unique_ptr already no-ops
on null). Locate the usage in compositor_core.cpp where the `keyboard`
unique_ptr is reset (search for the `keyboard` identifier in the function
surrounding the shown diff) and simplify accordingly.

In `@src/compositor/compositor_focus.cpp`:
- Around line 475-482: The helper get_surface_extent has external linkage but is
only used internally; change its linkage by either marking the function static
(add the static keyword to the get_surface_extent declaration/definition) or
move its definition into the file's anonymous namespace alongside helpers like
get_root_xdg_surface so it has internal linkage; update any references if you
relocate it and ensure the signature and return type remain unchanged.

In `@src/compositor/compositor_xdg.cpp`:
- Around line 170-203: The lock on hooks_mutex in
CompositorState::handle_xdg_popup_commit is held across the wlroots call
wlr_xdg_popup_unconstrain_from_box which can emit signals and cause deadlocks;
change the code to acquire hooks_mutex only to call get_popup_owner_root_surface
and compute a local wlr_box constraint_box (and copy any needed pointers/flags
such as hooks->popup) then release the lock before calling
wlr_xdg_popup_unconstrain_from_box, and only call the wlroots function when not
holding hooks_mutex (ensure any pointers used remain valid after the lock is
released).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24c12bd1-0a48-4ea9-a035-d105c20d32bd

📥 Commits

Reviewing files that changed from the base of the PR and between 19879ac and 7e247ab.

📒 Files selected for processing (5)
  • src/compositor/compositor_core.cpp
  • src/compositor/compositor_focus.cpp
  • src/compositor/compositor_layer_shell.cpp
  • src/compositor/compositor_xdg.cpp
  • src/compositor/compositor_xwayland.cpp

@K1ngst0m K1ngst0m merged commit 40f8b6d into main Mar 7, 2026
4 checks passed
@K1ngst0m K1ngst0m deleted the dev/split-compositor-src branch March 7, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant