atagen

monolith

public
Star0
Watch0

Commit d47a99c6ac9f

tarball

atagen <boss@atagen.co>2026-10-19 10:44 UTC

1 file changed+106-24
drm: change the mode of a live output in place

DrmScanEvent::Changed fires when the mode list changes and did a full disconnect and connect. That created a new wl_output, dropped the lock surface of a locked session, re-placed every window and forgot an explicit power-off. DrmOutput::use_mode now retargets the surface and the layout area, and the rebuild remains as the fallback when the mode cannot be applied or the monitor itself changed.
Modified file: crates/monolith/src/drm.rs+106 additions−24 deletions
@@ -319,10 +319,7 @@ fn scan(state: &mut State) {
319319 DrmScanEvent::Changed {
320320 connector,
321321 crtc: Some(crtc),
322- } => {
323- disconnect(state, crtc);
324- connect(state, connector, crtc);
325- }
322+ } => changed(state, connector, crtc),
326323 _ => {}
327324 }
328325 }
@@ -360,11 +357,7 @@ fn retry_connector(state: &mut State, handle: connector::Handle) {
360357 }
361358
362359 fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
363- let name = format!(
364- "{}-{}",
365- connector.interface().as_str(),
366- connector.interface_id()
367- );
360+ let name = connector_name(&connector);
368361 let config = state
369362 .settings
370363 .config
@@ -373,21 +366,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
373366 .get(&name)
374367 .cloned()
375368 .unwrap_or_default();
376- let chosen = config.mode.as_deref().and_then(|spec| {
377- let found = pick_mode(connector.modes(), spec);
378- if found.is_none() {
379- tracing::warn!(name, spec, "no such mode, using the preferred one");
380- }
381- found
382- });
383- let Some(mode) = chosen.or_else(|| {
384- connector
385- .modes()
386- .iter()
387- .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
388- .or_else(|| connector.modes().first())
389- .copied()
390- }) else {
369+ let Some(mode) = choose_mode(&connector, &name, config.mode.as_deref()) else {
391370 tracing::warn!(name, "connector has no modes");
392371 if let Some(drm) = state.backend.drm_mut() {
393372 drm.failed.insert(connector.handle());
@@ -508,6 +487,109 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
508487 );
509488 }
510489
490+fn connector_name(connector: &connector::Info) -> String {
491+ format!(
492+ "{}-{}",
493+ connector.interface().as_str(),
494+ connector.interface_id()
495+ )
496+}
497+
498+/// The configured mode if the connector has it, else its preferred one.
499+fn choose_mode(
500+ connector: &connector::Info,
501+ name: &str,
502+ configured: Option<&str>,
503+) -> Option<control::Mode> {
504+ let chosen = configured.and_then(|spec| {
505+ let found = pick_mode(connector.modes(), spec);
506+ if found.is_none() {
507+ tracing::warn!(name, spec, "no such mode, using the preferred one");
508+ }
509+ found
510+ });
511+ chosen.or_else(|| {
512+ connector
513+ .modes()
514+ .iter()
515+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
516+ .or_else(|| connector.modes().first())
517+ .copied()
518+ })
519+}
520+
521+fn changed(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
522+ if !remode(state, &connector, crtc) {
523+ disconnect(state, crtc);
524+ connect(state, connector, crtc);
525+ }
526+}
527+
528+/// Moves a lit output to the mode its connector now calls for without
529+/// recreating the wl_output, so clients, windows, the lock surface and the
530+/// power state stay as they are. False means the output has to be rebuilt.
531+fn remode(state: &mut State, connector: &connector::Info, crtc: crtc::Handle) -> bool {
532+ let name = connector_name(connector);
533+ let State {
534+ backend: Backend::Drm(drm),
535+ renderer,
536+ wm: Wm { layout, .. },
537+ settings,
538+ ..
539+ } = state
540+ else {
541+ return false;
542+ };
543+ let Some(s) = drm.surfaces.get_mut(&crtc) else {
544+ return false;
545+ };
546+ let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
547+ if s.output.physical_properties().size != (mm_w as i32, mm_h as i32).into() {
548+ return false;
549+ }
550+ let configured = settings.config.output.named.get(&name);
551+ let Some(mode) = choose_mode(connector, &name, configured.and_then(|c| c.mode.as_deref()))
552+ else {
553+ return false;
554+ };
555+ let wl_mode = Mode::from(mode);
556+ let moved = s.output.current_mode() != Some(wl_mode);
557+ if moved {
558+ let elements = DrmOutputRenderElements::default();
559+ if let Err(e) = s.drm.use_mode::<_, Draw>(mode, renderer, &elements) {
560+ tracing::warn!(name, "could not change the mode in place: {e}");
561+ return false;
562+ }
563+ s.output
564+ .change_current_state(Some(wl_mode), None, None, None);
565+ s.refresh_ns = 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32);
566+ s.last_vblank_ns = None;
567+ s.target_ns = None;
568+ s.redraw = true;
569+ let area = logical_area(
570+ s.output.current_location(),
571+ wl_mode.size,
572+ s.output.current_transform(),
573+ s.output.current_scale().fractional_scale(),
574+ );
575+ layout.set_output_area(s.id, area);
576+ tracing::info!(
577+ name,
578+ w = wl_mode.size.w,
579+ h = wl_mode.size.h,
580+ refresh = mode.vrefresh(),
581+ "output mode changed"
582+ );
583+ }
584+ s.output.set_preferred(wl_mode);
585+ if moved {
586+ reconfigure_outputs(state);
587+ state.reserve_layers();
588+ state.reconfigure_lock();
589+ }
590+ true
591+}
592+
511593 /// The mode matching `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`, the highest rate
512594 /// when none is given and the nearest one when it is.
513595 fn pick_mode(modes: &[control::Mode], spec: &str) -> Option<control::Mode> {