diff --git a/core/internal/server/wayland/manager.go b/core/internal/server/wayland/manager.go index 0255a717..3540e3db 100644 --- a/core/internal/server/wayland/manager.go +++ b/core/internal/server/wayland/manager.go @@ -3,8 +3,11 @@ package wayland import ( "bytes" "encoding/binary" + "errors" "fmt" + "io" "os" + "slices" "syscall" "time" @@ -73,7 +76,10 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error m.post(func() { log.Info("Gamma control enabled at startup") gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil { + m.availOutputsMu.RLock() + outs := slices.Clone(m.availableOutputs) + m.availOutputsMu.RUnlock() + if err := m.setupOutputControls(outs, gammaMgr); err != nil { log.Errorf("Failed to initialize gamma controls: %v", err) return } @@ -170,6 +176,7 @@ func (m *Manager) setupRegistry() error { }) if gammaMgr != nil { outputs = append(outputs, output) + m.addAvailableOutput(output) } m.outputRegNames.Store(outputID, e.Name) @@ -204,6 +211,11 @@ func (m *Manager) setupRegistry() error { } if foundOut.gammaControl != nil { foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() + foundOut.gammaControl = nil + } + m.removeAvailableOutput(foundOut.output) + if foundOut.output != nil && !foundOut.output.IsZombie() { + _ = foundOut.output.Release() } m.outputs.Delete(foundID) @@ -288,14 +300,28 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co if !ok { return } + if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() { + ctrl.Destroy() + } + out.gammaControl = nil out.failed = true out.rampSize = 0 out.retryCount++ out.lastFailTime = time.Now() + if !m.outputStillValid(out) { + return + } + backoff := time.Duration(300<= 10: + return nil + case !m.outputStillValid(out): return nil } if _, ok := m.outputs.Load(out.id); !ok { return nil } - if out.isVirtual { - return nil - } - if out.retryCount >= 10 { - return nil - } gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) if !ok { return fmt.Errorf("no gamma manager") } + if existing, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && existing != nil && !existing.IsZombie() { + existing.Destroy() + out.gammaControl = nil + } + control, err := gammaMgr.GetGammaControl(out.output) if err != nil { + if isConnectionDeadErr(err) { + m.markConnectionDead(err) + } return err } @@ -358,6 +458,13 @@ func (m *Manager) recreateOutputControl(out *outputState) error { return nil } +func (m *Manager) markConnectionDead(err error) { + if m.connectionDead.Swap(true) { + return + } + log.Errorf("gamma: wayland connection appears dead (%v); pausing gamma operations", err) +} + func (m *Manager) recalcSchedule(now time.Time) { m.configMutex.RLock() config := m.config @@ -690,11 +797,12 @@ func (m *Manager) applyGamma(temp int) { gamma := m.config.Gamma m.configMutex.RUnlock() - if !m.controlsInitialized { + switch { + case m.connectionDead.Load(): return - } - - if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma { + case !m.controlsInitialized: + return + case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma: return } @@ -714,7 +822,14 @@ func (m *Manager) applyGamma(temp int) { var jobs []job for _, out := range outs { - if out.failed || out.rampSize == 0 { + switch { + case out.failed: + continue + case out.rampSize == 0: + continue + case out.gammaControl == nil: + continue + case !m.outputStillValid(out): continue } ramp := GenerateGammaRamp(out.rampSize, temp, gamma) @@ -732,18 +847,16 @@ func (m *Manager) applyGamma(temp int) { } for _, j := range jobs { - if err := m.setGammaBytes(j.out, j.data); err != nil { - log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) - j.out.failed = true - j.out.rampSize = 0 - outID := j.out.id - time.AfterFunc(300*time.Millisecond, func() { - m.post(func() { - if out, ok := m.outputs.Load(outID); ok && out.failed { - m.recreateOutputControl(out) - } - }) - }) + err := m.setGammaBytes(j.out, j.data) + if err == nil { + continue + } + log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) + j.out.failed = true + j.out.rampSize = 0 + if isConnectionDeadErr(err) { + m.markConnectionDead(err) + return } } @@ -752,6 +865,14 @@ func (m *Manager) applyGamma(temp int) { } func (m *Manager) setGammaBytes(out *outputState, data []byte) error { + if out.gammaControl == nil { + return fmt.Errorf("no gamma control") + } + ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) + if !ok || ctrl == nil || ctrl.IsZombie() { + return fmt.Errorf("gamma control invalid") + } + fd, err := MemfdCreate("gamma-ramp", 0) if err != nil { return err @@ -774,7 +895,6 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error { } syscall.Seek(fd, 0, 0) - ctrl := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) return ctrl.SetGamma(fd) } @@ -882,10 +1002,10 @@ func (m *Manager) dbusMonitor() { } func (m *Manager) handleDBusSignal(sig *dbus.Signal) { - if sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep" { + switch { + case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep": return - } - if len(sig.Body) == 0 { + case len(sig.Body) == 0: return } preparing, ok := sig.Body[0].(bool) @@ -899,27 +1019,34 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) { return } time.AfterFunc(500*time.Millisecond, func() { - m.post(func() { - m.configMutex.RLock() - stillEnabled := m.config.Enabled - m.configMutex.RUnlock() - if !stillEnabled || !m.controlsInitialized { - return - } - m.outputs.Range(func(_ uint32, out *outputState) bool { - if out.gammaControl != nil { - out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() - out.gammaControl = nil - } - out.retryCount = 0 - out.failed = false - m.recreateOutputControl(out) - return true - }) - }) + m.post(m.handleResume) }) } +func (m *Manager) handleResume() { + m.configMutex.RLock() + stillEnabled := m.config.Enabled + m.configMutex.RUnlock() + + switch { + case !stillEnabled: + return + case !m.controlsInitialized: + return + case m.connectionDead.Load(): + return + } + + // Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma + // ramp to DRM on resume; gamma_control objects stay valid. We just need + // to force a resend so the schedule catches up with the current time of + // day — the original #1235 regression was caused by lastAppliedTemp + // matching and the send being skipped. + m.recalcSchedule(time.Now()) + m.lastAppliedTemp = 0 + m.applyCurrentTemp("resume") +} + func (m *Manager) triggerUpdate() { select { case m.updateTrigger <- struct{}{}: @@ -1058,7 +1185,10 @@ func (m *Manager) SetEnabled(enabled bool) { case enabled && !m.controlsInitialized: m.post(func() { gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil { + m.availOutputsMu.RLock() + outs := slices.Clone(m.availableOutputs) + m.availOutputsMu.RUnlock() + if err := m.setupOutputControls(outs, gammaMgr); err != nil { log.Errorf("gamma: failed to create controls: %v", err) return } diff --git a/core/internal/server/wayland/types.go b/core/internal/server/wayland/types.go index d624ca80..31bac151 100644 --- a/core/internal/server/wayland/types.go +++ b/core/internal/server/wayland/types.go @@ -3,6 +3,7 @@ package wayland import ( "math" "sync" + "sync/atomic" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" @@ -71,9 +72,11 @@ type Manager struct { registry *wlclient.Registry gammaControl any availableOutputs []*wlclient.Output + availOutputsMu sync.RWMutex outputRegNames syncmap.Map[uint32, uint32] outputs syncmap.Map[uint32, *outputState] controlsInitialized bool + connectionDead atomic.Bool cmdq chan cmd alive bool diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index 475aea16..d3d38722 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -819,6 +819,7 @@ Singleton { if (event.event === "unlock" || event.event === "resume") { suppressOsd = true; osdSuppressTimer.restart(); + evaluateNightMode(); } }