Skip to content

Commit

Permalink
feat: initial support for gamma
Browse files Browse the repository at this point in the history
In an effort to support more color correction options, add gamma color
gain settings. See #1

This does not include support for setting degamma/gamma exponents, as
there needs to be a user-friendly way of setting them.
  • Loading branch information
Scrumplex committed Aug 27, 2022
1 parent f37b3f3 commit 547a7b5
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 15 deletions.
54 changes: 43 additions & 11 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
import sys
import struct
import subprocess
from typing import List
from typing import List, Iterable

CTM_PROP = "GAMESCOPE_COLOR_MATRIX"
GAMMA_LGAIN_BLEND_PROP = "GAMESCOPE_COLOR_LINEARGAIN_BLEND"
GAMMA_LGAIN_PROP = "GAMESCOPE_COLOR_LINEARGAIN"
GAMMA_GAIN_PROP = "GAMESCOPE_COLOR_GAIN"


def saturation_to_coeffs(saturation: float) -> List[float]:
coeff = (1.0 - saturation) / 3.0
Expand All @@ -33,12 +37,31 @@ def saturation_to_coeffs(saturation: float) -> List[float]:
coeffs[8] += saturation
return coeffs


def float_to_long(x: float) -> int:
return struct.unpack("!I", struct.pack("!f", x))[0]


def long_to_float(x: int) -> float:
return struct.unpack("!f", struct.pack("!I", x))[0]


def set_cardinal_prop(prop_name: str, values: Iterable[int]):

param = ",".join(map(str, values))

command = ["xprop", "-root", "-f", prop_name,
"32c", "-set", prop_name, param]

if "DISPLAY" not in os.environ:
command.insert(1, ":1")
command.insert(1, "-display")

completed = subprocess.run(command, stderr=sys.stderr, stdout=sys.stdout)

return completed.returncode == 0


class Plugin:

async def set_saturation(self, saturation: float):
Expand All @@ -49,20 +72,29 @@ async def set_saturation(self, saturation: float):
coeffs = saturation_to_coeffs(saturation)

# represent floats as longs
long_coeffs = map(str, map(float_to_long, coeffs))
# concatenate longs to comma-separated list for xprop
ctm_param = ",".join(list(long_coeffs))
long_coeffs = map(float_to_long, coeffs)

command = ["xprop", "-root", "-f", CTM_PROP, "32c", "-set", CTM_PROP, ctm_param]
return set_cardinal_prop(CTM_PROP, long_coeffs)

if "DISPLAY" not in os.environ:
command.insert(1, ":1")
command.insert(1, "-display")
# values = 3 floats, R, G and B values respectively
async def set_gamma_linear_gain(self, values: List[float]):
long_values = map(float_to_long, values)

return set_cardinal_prop(GAMMA_LGAIN_PROP, long_values)

# values = 3 floats, R, G and B values respectively
async def set_gamma_gain(self, values: List[float]):
long_values = map(float_to_long, values)

return set_cardinal_prop(GAMMA_GAIN_PROP, long_values)

completed = subprocess.run(command, stderr=sys.stderr, stdout=sys.stdout)
# value = weight of lineargain (1.0 means that only linear gain is used) (0.0 <= value <= 1.0)
async def set_gamma_linear_gain_blend(self, value: float):
long_value = float_to_long(value)

return completed.returncode == 0
return set_cardinal_prop(GAMMA_LGAIN_BLEND_PROP, [long_value])

# TODO make this generic
async def get_saturation(self) -> float:
command = ["xprop", "-root", CTM_PROP]

Expand All @@ -80,7 +112,7 @@ async def get_saturation(self) -> float:
# "1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216"
ctm_param = stdout.split("=")[1]
# [1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216]
long_coeffs = list(map(int, ctm_param.split(",")));
long_coeffs = list(map(int, ctm_param.split(",")))
# [1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0]
coeffs = list(map(long_to_float, long_coeffs))
# 1.0
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vibrantDeck",
"author": "Scrumplex",
"flags": [],
"flags": ["debug"],
"publish": {
"tags": ["vibrant", "saturation"],
"description": "Set vibrancy (saturation) of your screen",
Expand Down
97 changes: 94 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from "decky-frontend-lib";
import { VFC, useState, useEffect } from "react";
import { FaEyeDropper } from "react-icons/fa";
import { loadSettingsFromLocalStorage, Settings, saveSettingsToLocalStorage } from "./settings";
import { loadSettingsFromLocalStorage, Settings, saveSettingsToLocalStorage, GammaSetting } from "./settings";
import { RunningApps, Backend, DEFAULT_APP } from "./util";

// Appease TypeScript
Expand All @@ -41,6 +41,10 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void
const [currentAppOverride, setCurrentAppOverride] = useState<boolean>(false);
const [currentAppOverridable, setCurrentAppOverridable] = useState<boolean>(false);
const [currentTargetSaturation, setCurrentTargetSaturation] = useState<number>(100);
const [currentTargetGammaLinear, setCurrentTargetGammaLinear] = useState<boolean>(true);
const [currentTargetGammaRed, setCurrentTargetGammaRed] = useState<number>(100);
const [currentTargetGammaGreen, setCurrentTargetGammaGreen] = useState<number>(100);
const [currentTargetGammaBlue, setCurrentTargetGammaBlue] = useState<number>(100);

const refresh = () => {
const activeApp = RunningApps.active();
Expand All @@ -50,6 +54,10 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void

// get configured saturation for current app (also Deck UI!)
setCurrentTargetSaturation(settings.appSaturation(activeApp));
setCurrentTargetGammaLinear(settings.appGamma(activeApp).linear);
setCurrentTargetGammaRed(settings.appGamma(activeApp).gain_r);
setCurrentTargetGammaGreen(settings.appGamma(activeApp).gain_g);
setCurrentTargetGammaBlue(settings.appGamma(activeApp).gain_b);

setInitialized(true);
}
Expand All @@ -71,6 +79,29 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void
saveSettingsToLocalStorage(settings);
}, [currentTargetSaturation, initialized]);

useEffect(() => {
const activeApp = RunningApps.active();
if (!initialized)
return;

if (currentAppOverride && currentAppOverridable) {
console.log(`Setting app ${activeApp} to${currentTargetGammaLinear ? " linear" : ""} gamma ${currentTargetGammaRed} ${currentTargetGammaGreen} ${currentTargetGammaBlue}`);
settings.ensureApp(activeApp).ensureGamma().linear = currentTargetGammaLinear;
settings.ensureApp(activeApp).ensureGamma().gain_r = currentTargetGammaRed;
settings.ensureApp(activeApp).ensureGamma().gain_g = currentTargetGammaGreen;
settings.ensureApp(activeApp).ensureGamma().gain_b = currentTargetGammaBlue;
} else {
console.log(`Setting global to${currentTargetGammaLinear ? " linear" : ""} gamma ${currentTargetGammaRed} ${currentTargetGammaGreen} ${currentTargetGammaBlue}`);
settings.ensureApp(DEFAULT_APP).ensureGamma().linear = currentTargetGammaLinear;
settings.ensureApp(DEFAULT_APP).ensureGamma().gain_r = currentTargetGammaRed;
settings.ensureApp(DEFAULT_APP).ensureGamma().gain_g = currentTargetGammaGreen;
settings.ensureApp(DEFAULT_APP).ensureGamma().gain_b = currentTargetGammaBlue;
}
applyFn(activeApp);

saveSettingsToLocalStorage(settings);
}, [currentTargetGammaLinear, currentTargetGammaRed, currentTargetGammaGreen, currentTargetGammaBlue, initialized]);

useEffect(() => {
const activeApp = RunningApps.active();
if (!initialized)
Expand All @@ -83,6 +114,10 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void
if (!currentAppOverride) {
settings.ensureApp(activeApp).saturation = undefined;
setCurrentTargetSaturation(settings.appSaturation(DEFAULT_APP));
setCurrentTargetGammaLinear(settings.appGamma(DEFAULT_APP).linear);
setCurrentTargetGammaRed(settings.appGamma(DEFAULT_APP).gain_r);
setCurrentTargetGammaGreen(settings.appGamma(DEFAULT_APP).gain_g);
setCurrentTargetGammaBlue(settings.appGamma(DEFAULT_APP).gain_b);
}
saveSettingsToLocalStorage(settings);
}, [currentAppOverride, initialized]);
Expand All @@ -93,7 +128,7 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void
}, []);

return (
<PanelSection title="Color Settings">
<PanelSection title="Profile">
<PanelSectionRow>
<ToggleField
label="Use per-game profile"
Expand All @@ -119,6 +154,58 @@ const Content: VFC<{ runningApps: RunningApps, applyFn: (appId: string) => void
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
label="Linear Gamma Gain"
description={"Use linear gamma scale"}
checked={currentTargetGammaLinear}
onChange={(linear) => {
setCurrentTargetGammaLinear(linear);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
label="Gamma Red"
description={`Control${currentTargetGammaLinear ? " linear" : ""} gamma gain for red`}
value={currentTargetGammaRed}
step={5}
max={900}
min={-50}
showValue={true}
onChange={(value: number) => {
setCurrentTargetGammaRed(value);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
label="Gamma Green"
description={`Control${currentTargetGammaLinear ? " linear" : ""} gamma gain for green´`}
value={currentTargetGammaGreen}
step={5}
max={900}
min={-50}
showValue={true}
onChange={(value: number) => {
setCurrentTargetGammaGreen(value);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
label="Gamma Blue"
description={`Control${currentTargetGammaLinear ? " linear" : ""} gamma gain for blue`}
value={currentTargetGammaBlue}
step={5}
max={900}
min={-50}
showValue={true}
onChange={(value: number) => {
setCurrentTargetGammaBlue(value);
}}
/>
</PanelSectionRow>
</PanelSection>
);
};
Expand All @@ -133,6 +220,8 @@ export default definePlugin((serverAPI: ServerAPI) => {
const applySettings = (appId: string) => {
const saturation = settings.appSaturation(appId);
backend.applySaturation(saturation);
const gamma = settings.appGamma(appId);
backend.applyGamma(gamma);
};

runningApps.register();
Expand All @@ -146,7 +235,9 @@ export default definePlugin((serverAPI: ServerAPI) => {
icon: <FaEyeDropper />,
onDismount() {
runningApps.unregister();
backend.applySaturation(100); // reset saturation if we won't be running anymore
// reset color settings to default values
backend.applySaturation(100);
backend.applyGamma(new GammaSetting());
}
};
});
27 changes: 27 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,36 @@ const SETTINGS_KEY = "vibrantDeck";

const serializer = new JsonSerializer();

@JsonObject()
export class GammaSetting {
@JsonProperty()
linear: boolean = true;
@JsonProperty()
gain_r: number = 100;
@JsonProperty()
gain_g: number = 100;
@JsonProperty()
gain_b: number = 100;
}

@JsonObject()
export class AppSetting {
@JsonProperty()
saturation?: number;
@JsonProperty()
gamma?: GammaSetting;

ensureGamma(): GammaSetting {
if (this.gamma == undefined)
this.gamma = new GammaSetting();
return this.gamma;
}

hasSettings(): boolean {
if (this.saturation != undefined)
return true;
if (this.gamma != undefined)
return true;
return false;
}
}
Expand All @@ -37,6 +59,11 @@ export class Settings {
return this.perApp[DEFAULT_APP].saturation!!;
return 100;
}

appGamma(appId: string) {
// app gamma or global gamma or fallback to defaults
return this.perApp[appId]?.gamma || this.perApp[DEFAULT_APP]?.gamma || new GammaSetting();
}
}

export function loadSettingsFromLocalStorage(): Settings {
Expand Down
23 changes: 23 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Router, ServerAPI } from "decky-frontend-lib";
import { GammaSetting } from "./settings";

interface SaturationArgs {
saturation: number
}
interface GammaGainArgs {
values: number[]
}
interface GammaBlendArgs {
value: number
}

type ActiveAppChangedHandler = (newAppId: string, oldAppId: string) => void;
type UnregisterFn = () => void;
Expand Down Expand Up @@ -57,4 +64,20 @@ export class Backend {
console.log("Applying saturation " + saturation.toString());
this.serverAPI.callPluginMethod<SaturationArgs, boolean>("set_saturation", { "saturation": saturation / 100.0 });
}

applyGamma(gamma: GammaSetting) {
const defaults = new GammaSetting();
const default_values = [defaults.gain_r / 100.0, defaults.gain_g / 100.0, defaults.gain_b / 100.0]
const values = [gamma.gain_r / 100.0, gamma.gain_g / 100.0, gamma.gain_b / 100.0];
console.log(`Applying gamma ${gamma.linear ? "linear" : ""} gain ${values.toString()}`);
if (gamma.linear) {
this.serverAPI.callPluginMethod<GammaGainArgs, boolean>("set_gamma_gain", { "values": default_values });
this.serverAPI.callPluginMethod<GammaGainArgs, boolean>("set_gamma_linear_gain", { "values": values });
this.serverAPI.callPluginMethod<GammaBlendArgs, boolean>("set_gamma_linear_gain_blend", { "value": 1.0 });
} else {
this.serverAPI.callPluginMethod<GammaGainArgs, boolean>("set_gamma_gain", { "values": values });
this.serverAPI.callPluginMethod<GammaGainArgs, boolean>("set_gamma_linear_gain", { "values": default_values });
this.serverAPI.callPluginMethod<GammaBlendArgs, boolean>("set_gamma_linear_gain_blend", { "value": 0.0 });
}
}
}

0 comments on commit 547a7b5

Please sign in to comment.