How Hyprland Handles Monitors and Display Configurations: A Technical Deep-Dive

Hyprland implements monitor management by encapsulating each physical or virtual display as a CMonitor object, coordinating detection, configuration, and lifecycle events through specialized state trackers and rule managers that interface directly with the Aquamarine backend.

The hyprwm/Hyprland repository defines a sophisticated display architecture that transforms raw Wayland outputs into fully configured monitors. At its core, the system treats every screen—physical or virtual—as a discrete CMonitor instance that owns its Aquamarine output, manages current display modes, and emits lifecycle events during connection, configuration changes, and disconnection.

Core Components of the Monitor Ecosystem

The display stack relies on several tightly-coupled components defined across the source tree:

  • CMonitor – The runtime representation of a display located in src/output/Monitor.cpp. It owns the Aquamarine output, maintains current mode and scaling state, handles color management, and drives the per-monitor frame scheduler.
  • CMonitorStateTracker – A global registry declared in src/state/MonitorState.hpp that stores all monitors in m_realMonitors and the logical working list m_monitors. It provides the monitorState() accessor used throughout the codebase to query or modify display sets.
  • MonitorRuleManager – Parses user-defined monitor= configuration rules from src/config/shared/monitor/MonitorRuleManager.cpp, resolving which CMonitorRule applies to which output during connection.
  • MonitorLayoutController – Arranges monitors in a rectangular coordinate space after add/remove operations, triggering window and workspace relayouts.
  • WorkspacePlacementController – Ensures workspaces migrate to available monitors when displays appear or disappear.
  • MonitorFrameScheduler – Drives the per-monitor frame-callback chain in src/output/MonitorFrameScheduler.cpp, handling rendering loops and DPMS events.

Monitor Detection and the Connection Lifecycle

When Aquamarine signals a new IOutput, Hyprland instantiates a CMonitor via the constructor in src/output/Monitor.cpp. The object registers listeners for frame, commit, present, destroy, and state events. The full connection sequence executes inside CMonitor::onConnect (lines 84–90 and 108–124):

  1. Emit pre-add event – Event::bus()->m_events.monitor.preAdded.emit notifies listeners before structural changes occur.
  2. Apply the monitor rule – The system fetches the matching rule via Config::monitorRuleMgr()->get(this) and invokes applyMonitorRule.
  3. Enable the output – m_output->state->setEnabled(true) activates the display backend, followed by m_state.commit().
  4. Create frame scheduler – m_frameScheduler = makeUnique<CMonitorFrameScheduler>(…) initializes rendering timing.
  5. Configure advanced features – Mirroring, Variable Refresh Rate (VRR), gamma tables, and color-management profiles are applied.
  6. Place workspaces – WorkspacePlacementController restores any workspaces previously associated with this monitor ID.
  7. Emit added events – g_pEventManager->postEvent({ "monitoradded", … }) and Event::bus()->m_events.monitor.added.emit broadcast the new display to the ecosystem.

Resolution and Mode Selection

The heart of display configuration resides in CMonitor::applyMonitorRule (lines 774–820 and 842–870). Hyprland builds a list of candidate modes based on the user’s rule and evaluates them against hardware capabilities:

  • Preferred mode – Used when the rule specifies no resolution.
  • Sentinel values – Special tokens like -1,-1 (preferred), -1,-2 (highest refresh rate), and -1,-3 (highest resolution/maximum width) trigger custom sorting of advertised modes.
  • Exact specifications – User-defined resolutions or custom DRM modelines (DRM_MODE_TYPE_USERDEF) are validated directly.

The algorithm stores the three best candidates in requestedModes, then iterates in reverse order:

m_state.applyModeWithSwapchain(mode);   // or applyCustomModeWithSwapchain for user-defined modes
if (!m_state.test()) continue;            // Reject if the compositor cannot commit

If m_state.test() succeeds, Hyprland updates m_refreshRate, m_size, m_currentMode, and m_pixelSize, schedules a frame, and finalizes with m_state.commit(). Should all candidates fail, the system invokes scheduleModeRetry to reattempt configuration up to three times after short delays.

Scaling, Transforms, and Color Management

After mode selection, Hyprland applies geometric and colorimetric transformations in applyMonitorRuleSoft:

Scaling – If the rule’s scale value is ≤ 0.1, Hyprland enables autoScale and selects the hardware’s getDefaultScale(). For explicit scales, the system validates that the value divides the pixel dimensions cleanly. When a user-supplied scale is invalid, Hyprland searches for the nearest valid divisor in steps of 1/120; if none exists, it falls back to the default scale and optionally notifies the user. This logic appears around line 990 of src/output/Monitor.cpp.

Transform – The rule’s rotation/flip value is applied via m_transform = RULE->m_transform;. Odd-valued transforms (90° or 270°) trigger a swap between width and height dimensions to maintain correct portrait/landscape orientation.

Color Management – The cm field in the monitor rule determines the color space. Based on hardware capabilities (m_enabled10bit, supportsWideColor(), supportsHDR()) and optional ICC profiles, applyCMType constructs a CImageDescription. Changes propagate via PROTO::colorManagement->onMonitorImageDescriptionChanged (lines 420–470 and 520–560).

Mirroring and Workspace Placement

When a configuration specifies mirror_of, CMonitor::applyMonitorRuleSoft invokes setMirror(m_activeMonitorRule.m_mirrorOf). The mirroring implementation:

  • Registers the monitor as a clone of the target display.
  • Shares the frame scheduler and damage buffers with the primary monitor.
  • Automatically clears mirroring relationships if the target monitor disconnects, triggering a configuration reload via MonitorRuleManager.

Simultaneously, the WorkspacePlacementController ensures workspace persistence by calling workspaceState()->rememberWorkspaceForMonitor during disconnections, allowing workspaces to return to their previous monitor when it reconnects.

Disconnection and Resource Cleanup

When Aquamarine destroys an output, CMonitor::onDisconnect executes the following cleanup sequence:

  1. Emits the pre-remove event to signal imminent destruction.
  2. Destroys OpenGL resources associated with the monitor.
  3. Migrates windows and workspaces to a fallback monitor using the layout controller.
  4. Persists the last workspace association via workspaceState()->rememberWorkspaceForMonitor for future restoration.
  5. Emits removal events and schedules a layout re-check to rebalance remaining displays.

Practical Configuration Examples

Defining Monitor Rules in hyprland.conf

The MonitorRuleManager parses each monitor= line into a CMonitorRule structure:


# Enable HDMI-A-1 at 1080p 144Hz with specific scale and workspace

monitor = HDMI-A-1,1920x1080@144,scale=1.0,transform=0,defaultworkspace=1

# Mirror DP-1 to HDMI-A-1 (shares frame buffer)

monitor = DP-1,2560x1440,scale=1.25,mirror_of=HDMI-A-1

# Disable the internal laptop display

monitor = eDP-1,disable

Runtime Control via hyprctl

Changes applied at runtime flow through the same rule-application pipeline:


# Query current display states

hyprctl monitors

# Set DP-1 to 4K@60 with 10-bit color depth

hyprctl dispatch exec monitorrule DP-1,3840x2160@60,enable10bit=1

# Disable a connected display

hyprctl dispatch exec monitorrule eDP-1,disable

Querying Monitor Geometry Client-Side

Wayland clients can retrieve display metadata through Hyprland’s IPC system, which exposes the internal CMonitor state:

// Example: Querying compositor state via Hyprland IPC (pseudo-API)
auto monitors = hyprctl::getMonitors(); // pseudo-API
for (auto& m : monitors) {
    std::cout << "Monitor " << m.name << " size " << m.width << "x" << m.height
              << " @ " << m.refresh << "Hz, scale " << m.scale << "\n";
}

Summary

  • CMonitor objects in src/output/Monitor.cpp encapsulate all runtime display state, from Aquamarine output handles to current color profiles.
  • MonitorRuleManager translates configuration file syntax into structured rules that drive mode selection, scaling, and mirroring decisions.
  • Mode selection uses a three-candidate fallback system with applyModeWithSwapchain and m_state.test() validation, supporting sentinel values like -1,-1 for automatic configuration.
  • Scaling validates against 1/120 granularity steps, falling back to hardware defaults when user values are incompatible.
  • Global state trackers (CMonitorStateTracker, MonitorLayoutController, WorkspacePlacementController) ensure atomic updates across the compositor when monitors connect or disconnect.

Frequently Asked Questions

How does Hyprland store and apply monitor configuration rules?

Hyprland stores user configurations as CMonitorRule objects managed by the MonitorRuleManager class in src/config/shared/monitor/MonitorRuleManager.cpp. When a monitor connects, CMonitor::onConnect fetches the applicable rule via Config::monitorRuleMgr()->get(this) and applies it through applyMonitorRule, which handles resolution, refresh rate, scaling, and positioning.

What happens when Hyprland detects a monitor but cannot apply the requested mode?

If m_state.test() fails for all candidate modes during applyMonitorRule, Hyprland invokes scheduleModeRetry to reattempt configuration up to three times. This retry mechanism accommodates temporary hardware initialization delays or DRM state synchronization issues before falling back to safe defaults.

How does Hyprland calculate monitor scaling when auto-scaling is disabled?

When auto-scaling is disabled or a specific scale is requested, Hyprland validates that the scale divides the pixel dimensions cleanly. If the user-specified scale is invalid, the system searches for the nearest valid divisor in increments of 1/120; if no suitable divisor exists, it reverts to getDefaultScale() and optionally displays a notification to the user.

What is the relationship between CMonitor and the Aquamarine backend?

Each CMonitor instance owns an Aquamarine IOutput handle (m_output), which abstracts the underlying DRM/KMS device. The CMonitor class translates high-level configuration rules (resolution, VRR, color management) into Aquamarine state commits via m_output->state and m_state.commit(), bridging the compositor’s logical display model with kernel-level display hardware control.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →