mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
feat(icc): treat the per-output temperature as the profile's reference white point
The per-output temperature was documented as the white point a profile was produced at, but an output with a profile ignored it entirely: the ramp was generated from the profile and the temperature only applied to outputs without one (or as the fallback when a profile failed to parse). A display calibrated at 7000K therefore got the profile ramp, and enabling the night light had no effect on it at all. - `ProfileRampWithTemp` composes the profile ramp with the ratio between the target temperature ramp and the reference ramp, so the temperature is the white point the profile describes and the night light shifts relative to it. - `applyGamma` passes the night light temperature as the target, and `noTempTarget` when the schedule is disabled, which leaves profiled outputs at their reference white point and drives plain outputs with the neutral ramp. - The re-apply sentinel stays 0, so it cannot collide with an applied temperature. Covered by TestProfileRampWithTemp (reference == target is a no-op, warmer and cooler targets move the expected channels). Also: `dms icc set-temp` help text describes the reference-white-point semantics.
This commit is contained in:
1 parent
648d1dd4fd
commit
77c9f6d885
4 files changed
+132
-19
No files matched your search
@@ -60,10 +60,13 @@ var iccSetTempCmd = &cobra.Command{
|
||||
Use: "set-temp <output> <kelvin>",
|
||||
Aliases: []string{"setTemp"},
|
||||
Short: "Set the color temperature for a specific output",
|
||||
Long: "Set the color temperature for a specific output, overriding the\n" +
|
||||
"night light temperature for that output only.\n\n" +
|
||||
"Accepts 1000-10000K; 0 removes the override so the output follows the\n" +
|
||||
"night light schedule again.",
|
||||
Long: "Set the reference white point of an output, in kelvin.\n\n" +
|
||||
"For outputs with an ICC profile this is the white point the profile was\n" +
|
||||
"produced at: the profile is applied as measured, and the night light\n" +
|
||||
"shifts from that reference instead of assuming 6500K. Outputs without a\n" +
|
||||
"profile use it as their color temperature directly.\n\n" +
|
||||
"Accepts 1000-10000K; 0 removes the override, so the output follows the\n" +
|
||||
"night light schedule with a 6500K reference.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runICCSetTemp,
|
||||
}
|
||||
@@ -348,10 +351,10 @@ func runICCSetTemp(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
if temp == 0 {
|
||||
fmt.Printf("Output '%s' follows the night light temperature again\n", outputName)
|
||||
fmt.Printf("Output '%s' uses a 6500K reference white point again\n", outputName)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Set output '%s' color temperature to %dK\n", outputName, temp)
|
||||
fmt.Printf("Set output '%s' reference white point to %dK\n", outputName, temp)
|
||||
}
|
||||
|
||||
// fetchICCOutputTemps asks the running server for the per-output overrides.
|
||||
|
||||
@@ -2,6 +2,8 @@ package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/icc"
|
||||
)
|
||||
|
||||
type GammaRamp struct {
|
||||
@@ -166,3 +168,39 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
|
||||
|
||||
return ramp
|
||||
}
|
||||
|
||||
// ProfileRampWithTemp builds the gamma ramp for an output that has an ICC
|
||||
// profile. The profile describes the display at its reference white point
|
||||
// (baseTemp), so a temperature target is composed as the ratio between the
|
||||
// target ramp and the base ramp. targetTemp <= 0 means "no target", which
|
||||
// leaves the profile as measured.
|
||||
func ProfileRampWithTemp(size uint32, profile *icc.Profile, baseTemp, targetTemp int, gamma, contrast float64) (GammaRamp, error) {
|
||||
iccRamp, err := icc.GenerateGammaRamp(size, profile)
|
||||
if err != nil {
|
||||
return GammaRamp{}, err
|
||||
}
|
||||
ramp := GammaRamp{Red: iccRamp.Red, Green: iccRamp.Green, Blue: iccRamp.Blue}
|
||||
if targetTemp <= 0 || targetTemp == baseTemp {
|
||||
return ramp, nil
|
||||
}
|
||||
|
||||
base := GenerateGammaRamp(size, baseTemp, gamma, contrast)
|
||||
target := GenerateGammaRamp(size, targetTemp, gamma, contrast)
|
||||
|
||||
for i := range ramp.Red {
|
||||
ramp.Red[i] = scaleRampValue(ramp.Red[i], base.Red[i], target.Red[i])
|
||||
ramp.Green[i] = scaleRampValue(ramp.Green[i], base.Green[i], target.Green[i])
|
||||
ramp.Blue[i] = scaleRampValue(ramp.Blue[i], base.Blue[i], target.Blue[i])
|
||||
}
|
||||
|
||||
return ramp, nil
|
||||
}
|
||||
|
||||
// scaleRampValue rescales a profile ramp value by the ratio of the target and
|
||||
// base temperature ramps at the same index.
|
||||
func scaleRampValue(value, base, target uint16) uint16 {
|
||||
if base == 0 {
|
||||
return value
|
||||
}
|
||||
return uint16(clamp01(float64(value)*float64(target)/float64(base)/65535.0) * 65535.0)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/icc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
@@ -156,3 +158,59 @@ func TestGenerateGammaRamp_ContrastPivotsAtMidGray(t *testing.T) {
|
||||
t.Errorf("contrast 0.5 should compress endpoints to quarter points, got %d and %d", low.Red[0], low.Red[size-1])
|
||||
}
|
||||
}
|
||||
|
||||
// The per-output temperature is the white point the ICC profile was produced
|
||||
// at, so it only shifts the ramp when the night light asks for a different
|
||||
// temperature than that reference.
|
||||
func TestProfileRampWithTemp(t *testing.T) {
|
||||
const size = uint32(256)
|
||||
mid := int(size) / 2
|
||||
|
||||
// An empty profile yields the identity ramp.
|
||||
profile := &icc.Profile{}
|
||||
identity := GenerateIdentityRamp(size)
|
||||
|
||||
t.Run("no target keeps the profile as measured", func(t *testing.T) {
|
||||
for _, target := range []int{noTempTarget, 7000} {
|
||||
ramp, err := ProfileRampWithTemp(size, profile, 7000, target, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("target %d: ProfileRampWithTemp: %v", target, err)
|
||||
}
|
||||
if !slices.Equal(identity.Red, ramp.Red) || !slices.Equal(identity.Blue, ramp.Blue) {
|
||||
t.Fatalf("target %d: profile ramp should be unchanged at its own white point", target)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("warmer target than the reference warms the output", func(t *testing.T) {
|
||||
ramp, err := ProfileRampWithTemp(size, profile, 7000, 4000, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileRampWithTemp: %v", err)
|
||||
}
|
||||
if ramp.Red[mid] <= identity.Red[mid] {
|
||||
t.Errorf("red should be lifted: got %d, want > %d", ramp.Red[mid], identity.Red[mid])
|
||||
}
|
||||
if ramp.Green[mid] >= identity.Green[mid] {
|
||||
t.Errorf("green should be pulled down: got %d, want < %d", ramp.Green[mid], identity.Green[mid])
|
||||
}
|
||||
if ramp.Blue[mid] >= identity.Blue[mid] {
|
||||
t.Errorf("blue should be pulled down: got %d, want < %d", ramp.Blue[mid], identity.Blue[mid])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cooler target than the reference cools the output", func(t *testing.T) {
|
||||
ramp, err := ProfileRampWithTemp(size, profile, 6500, 10000, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileRampWithTemp: %v", err)
|
||||
}
|
||||
if ramp.Red[mid] >= identity.Red[mid] {
|
||||
t.Errorf("red should be pulled down: got %d, want < %d", ramp.Red[mid], identity.Red[mid])
|
||||
}
|
||||
if ramp.Green[mid] >= identity.Green[mid] {
|
||||
t.Errorf("green should be pulled down: got %d, want < %d", ramp.Green[mid], identity.Green[mid])
|
||||
}
|
||||
if ramp.Blue[mid] != identity.Blue[mid] {
|
||||
t.Errorf("blue is the reference channel and should be unchanged: got %d, want %d", ramp.Blue[mid], identity.Blue[mid])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -27,6 +27,11 @@ const animKelvinStep = 25
|
||||
|
||||
const neutralTemp = 6500
|
||||
|
||||
// noTempTarget marks "the night light has no temperature target". Plain outputs
|
||||
// then get the neutral ramp, while outputs with an ICC profile stay at the
|
||||
// white point their profile was produced at.
|
||||
const noTempTarget = -1
|
||||
|
||||
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -837,11 +842,10 @@ func (m *Manager) applyCurrentTemp(_ string) {
|
||||
low, high := m.config.LowTemp, m.config.HighTemp
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
// With the night light disabled the outputs still need their ICC ramp, so
|
||||
// push a neutral temperature: outputs with a profile get their measured
|
||||
// ramp, the rest stay at identity.
|
||||
// With the night light disabled, outputs with an ICC profile keep the ramp
|
||||
// their profile describes and the rest stay at identity (noTempTarget).
|
||||
if !enabled {
|
||||
m.applyGamma(neutralTemp)
|
||||
m.applyGamma(noTempTarget)
|
||||
m.updateStateFromSchedule()
|
||||
return
|
||||
}
|
||||
@@ -905,24 +909,34 @@ func (m *Manager) applyGamma(temp int) {
|
||||
case !m.outputStillValid(out):
|
||||
continue
|
||||
}
|
||||
// Per-output temperature doubles as the white point the profile was
|
||||
// produced at; without one, the profile is assumed to be at neutral.
|
||||
outBaseTemp := out.outputTemp
|
||||
if outBaseTemp == 0 {
|
||||
outBaseTemp = neutralTemp
|
||||
}
|
||||
|
||||
var ramp GammaRamp
|
||||
if out.iccPath != "" && out.iccProfile != nil {
|
||||
// ICC profile is active for this output — use ICC ramp
|
||||
iccRamp, err := icc.GenerateGammaRamp(out.rampSize, out.iccProfile)
|
||||
profileRamp, err := ProfileRampWithTemp(out.rampSize, out.iccProfile, outBaseTemp, temp, gamma, contrast)
|
||||
if err != nil {
|
||||
log.Warnf("icc: failed to generate ramp for output %d: %v, falling back to temperature", out.id, err)
|
||||
outTemp := temp
|
||||
if out.outputTemp != 0 {
|
||||
outTemp = out.outputTemp
|
||||
fallbackTemp := temp
|
||||
if fallbackTemp <= 0 {
|
||||
fallbackTemp = outBaseTemp
|
||||
}
|
||||
ramp = GenerateGammaRamp(out.rampSize, outTemp, gamma, contrast)
|
||||
ramp = GenerateGammaRamp(out.rampSize, fallbackTemp, gamma, contrast)
|
||||
} else {
|
||||
ramp = GammaRamp{Red: iccRamp.Red, Green: iccRamp.Green, Blue: iccRamp.Blue}
|
||||
log.Infof("icc: applied ICC ramp to output %d (size=%d)", out.id, out.rampSize)
|
||||
ramp = profileRamp
|
||||
log.Infof("icc: applied ICC ramp to output %d (size=%d, ref=%dK, target=%dK)", out.id, out.rampSize, outBaseTemp, temp)
|
||||
}
|
||||
} else {
|
||||
// No ICC profile — use per-output temp if set, otherwise global temp
|
||||
// No ICC profile — use per-output temp if set, otherwise the night
|
||||
// light temperature, otherwise neutral.
|
||||
outTemp := temp
|
||||
if outTemp <= 0 {
|
||||
outTemp = neutralTemp
|
||||
}
|
||||
if out.outputTemp != 0 {
|
||||
outTemp = out.outputTemp
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user