fix(icc): keep the attached profile when a gamma control is re-created

`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.
This commit is contained in:
KIDult2226
2026-09-14 13:04:05 +08:00
parent d09daaf177
commit 48866b3e27
2 changed files with 66 additions and 4 deletions
+28 -4
View File
@@ -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) {
@@ -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) {