From 48866b3e271f7beecd2e440d60730e593a441eb7 Mon Sep 17 00:00:00 2001 From: KIDult2226 <22439293@life.hkbu.edu.hk> Date: Mon, 14 Sep 2026 13:04:05 +0800 Subject: [PATCH] fix(icc): keep the attached profile when a gamma control is re-created MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `setupOutputControls` stored a fresh `outputState` for every output it touched, and `createControls` calls it whenever the controls are rebuilt, so recreating a control dropped the output's `iccPath`/`iccProfile`/`outputTemp` — configured state that does not belong to the control. The path is dormant today because `destroyControls` only runs when nothing is configured (`needsControls()` covers `ICCProfiles`/`OutputTemps`), but the snapshot published after `createControls` would then report whatever the rebuilt state holds instead of the profile the display is configured with. `controlStateFor` reuses the state of a known output and resets only what belongs to the new control: the ramp bookkeeping and the failure budget (rampSize, failed, retryCount, lastFailTime, lastTemp, lastGamma, lastContrast). A control that has not reported gamma_size yet has to receive its ramp again, while the profile and the per-output temperature survive. An output seen for the first time still starts from a clean state. Covered by TestManager_ControlStateReuseKeepsAttachedICC. --- core/internal/server/wayland/manager.go | 32 ++++++++++++++--- core/internal/server/wayland/manager_test.go | 38 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/core/internal/server/wayland/manager.go b/core/internal/server/wayland/manager.go index 764250cb2..fa1013479 100644 --- a/core/internal/server/wayland/manager.go +++ b/core/internal/server/wayland/manager.go @@ -267,16 +267,40 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g } outputID := output.ID() registryName, _ := m.outputRegNames.Load(outputID) - outState := &outputState{ + outState := m.controlStateFor(outputID, registryName, output, control) + m.setupControlHandlers(outState, control) + m.outputs.Store(outputID, outState) + } + return nil +} + +// controlStateFor returns the state a (re)created gamma control is attached to. +// A re-created control must not drop what belongs to the output rather than to +// the control: the attached ICC profile and the per-output temperature survive, +// while the ramp bookkeeping and the failure budget start over so the new +// control receives a fresh ramp once it reports gamma_size. +func (m *Manager) controlStateFor(outputID, registryName uint32, output *wlclient.Output, control any) *outputState { + existing, ok := m.outputs.Load(outputID) + if !ok { + return &outputState{ id: outputID, registryName: registryName, output: output, gammaControl: control, } - m.setupControlHandlers(outState, control) - m.outputs.Store(outputID, outState) } - return nil + + existing.registryName = registryName + existing.output = output + existing.gammaControl = control + existing.rampSize = 0 + existing.failed = false + existing.retryCount = 0 + existing.lastFailTime = time.Time{} + existing.lastTemp = 0 + existing.lastGamma = 0 + existing.lastContrast = 0 + return existing } func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_control.ZwlrGammaControlV1) { diff --git a/core/internal/server/wayland/manager_test.go b/core/internal/server/wayland/manager_test.go index cd218cb9e..eca4a9b00 100644 --- a/core/internal/server/wayland/manager_test.go +++ b/core/internal/server/wayland/manager_test.go @@ -631,6 +631,44 @@ func TestEffectiveTempTarget(t *testing.T) { } } +// A re-created gamma control (night light toggled, outputs re-enumerated) must +// not drop the configured ICC state of the output it belongs to. +func TestManager_ControlStateReuseKeepsAttachedICC(t *testing.T) { + m := &Manager{} + existing := &outputState{ + id: 1, + registryName: 10, + iccPath: "/tmp/display.icm", + iccProfile: &icc.Profile{Description: "Test Display"}, + outputTemp: 7000, + rampSize: 256, + failed: true, + retryCount: 3, + lastFailTime: time.Now(), + lastTemp: 7000, + lastGamma: 1.1, + lastContrast: 0.9, + } + m.outputs.Store(1, existing) + + got := m.controlStateFor(1, 10, nil, "control-2") + + assert.Same(t, existing, got, "the output keeps its state across control re-creation") + assert.Equal(t, "/tmp/display.icm", got.iccPath, "the attached profile survives") + assert.Equal(t, 7000, got.outputTemp, "the per-output temperature survives") + assert.Equal(t, "control-2", got.gammaControl, "the new control is attached") + + assert.Zero(t, got.rampSize, "the new control has not reported gamma_size yet") + assert.False(t, got.failed) + assert.Zero(t, got.retryCount) + assert.Zero(t, got.lastTemp, "the ramp has to be written again") + + fresh := m.controlStateFor(2, 20, nil, "control-3") + assert.NotSame(t, existing, fresh) + assert.Empty(t, fresh.iccPath, "an output seen for the first time starts clean") + assert.Zero(t, fresh.outputTemp) +} + // The status payload is what the settings UI shows for a profile, so the // descriptive metadata has to be carried through. func TestManager_GetICCStatusDescribesProfile(t *testing.T) {