Skip to content

Commit

Permalink
Implement contrast control
Browse files Browse the repository at this point in the history
  • Loading branch information
averne committed Nov 3, 2024
1 parent ddf6417 commit 03d4c3b
Show file tree
Hide file tree
Showing 17 changed files with 131 additions and 74 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
**/env/

misc/patches
misc/icon.svg
!misc/default.ini
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export FZ_VERSION = 2.7.0
export FZ_VERSION = 2.8.0
export FZ_COMMIT = $(shell git rev-parse --short HEAD)
export FZ_TID = 0100000000000F12

Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# Fizeau

<p align="center"><img src="https://github.com/averne/Fizeau/assets/45773016/1944246d-df65-461f-be2a-df3359476673" height=300></p>
# <img src="https://github.com/user-attachments/assets/da73e423-2941-4869-a28b-dbf5a4d2db03" width=48> Fizeau

Adjust the color of the screen of your Nintendo Switch.

# Features
- Modify the color temperature, saturation and hue of the display.
- Selectively apply corrections to color channels, filter to one single component.
- Tone mapping with programmable gamma, luminance, and color range.
- Tone mapping with programmable contrast, gamma, luminance, and color range.
- Schedule settings to be applied at dusk/dawn, with smooth transitions.
- Configurable screen dimming.

# Images
<p float="left">
<img src="https://github.com/user-attachments/assets/9c3fd0a2-1af2-44a8-a9a2-445b36610d7e" width="413" />
<img src="https://github.com/user-attachments/assets/3564a11f-b7eb-4d01-a20e-0491443b7978" width="413" />
<img src="https://github.com/user-attachments/assets/bb469aca-fee4-4a4b-8616-1d3258d837b8" width="413" />
<img src="https://github.com/user-attachments/assets/05eb1658-022f-4f72-a552-dfc62d476107" width="413" />
<img src="https://github.com/averne/Fizeau/assets/45773016/a5c4e32d-c7cd-4ef4-8298-d33e4984b79a" width="413" />
</p>

Expand Down Expand Up @@ -48,7 +46,7 @@ This software uses the CMU (Color Management Unit) built into the Tegra GPU of t
The CMU works in 3 passes:
- the first pass converts 8-bit sRGB data into a 12-bit linear colorspace, using a LUT (look-up table). Therefore, it also increases the precision of the color data.
- the second pass is a dot product between the CSC (Color Space Correction) matrix and the RGB data, as illustrated below.
<p align="center"><img src="https://i.imgur.com/qrO2Xdo.png"></p>
<p align="center"><img src="https://i.imgur.com/qrO2Xdo.png" width=300></p>
- the third pass maps the 12-bit linear, now corrected color data back into 8-bit sRGB, using another LUT. Hence the precision after this pass is decreased. Moreover, this LUT is split into two parts: the first 512 entries map [0, 32), while the 448 other represent [32, 256). This allows greater precision for darker color components, which the human eye is more sensitive to.

Overview of the CMU pipeline:
Expand All @@ -58,13 +56,13 @@ More detail can be found in the TRM (Tegra Reference Manual), section 24.3.14 (D

Official software use the CMU for multiple purposes (the following images were generated using a script found [here](misc/cmu.py), with data dumped from running official software):
- Represented below is the default CMU configuration. No correction is applied:
<p align="center"><img src="https://i.imgur.com/bSfIao7.png"></p>
<p align="center"><img src="https://i.imgur.com/bSfIao7.png" width=500></p>
- Color inversion is applied using the LUT2, which is simply inverted:
<p align="center"><img src="https://i.imgur.com/U7oSJYl.png"></p>
<p align="center"><img src="https://i.imgur.com/U7oSJYl.png" width=500></p>
- Grayscale is implemented with the CSC, using the luminance function Y = 0.2126 * R + 0.7152 * G + 0.0722 * B on each component (the actual coefficients being 0.2109375, 0.7109375 and 0.0703125 due to the limited precision of CSC components which are 10-bit signed [Q1.8](https://en.wikipedia.org/wiki/Q_(number_format)) numbers):
<p align="center"><img src="https://i.imgur.com/UfviH8k.png"></p>
<p align="center"><img src="https://i.imgur.com/UfviH8k.png" width=500></p>
- Lastly, luminance correction, here with a luma=1.0 (color range correction is very similar):
<p align="center"><img src="https://i.imgur.com/fsLv1wr.png"></p>
<p align="center"><img src="https://i.imgur.com/fsLv1wr.png" width=500></p>

In addition, the color range of the external display is restricted using the HDMI AVI (Auxiliary Video Information) infoframe.

Expand Down
10 changes: 7 additions & 3 deletions application/src/gfx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,13 @@ void render_preview(FizeauSettings &settings, Component components, Component fi
auto coeffs = filter_matrix(filter);

// Apply temperature color correction
ColorMatrix wp = {};
std::tie(wp[0], wp[4], wp[8]) = whitepoint(settings.temperature);
coeffs = dot(coeffs, wp);
ColorMatrix m = {};
std::tie(m[0], m[4], m[8]) = whitepoint(settings.temperature);
coeffs = dot(coeffs, m);

// Apply contrast multiplier
m[0] = m[4] = m[8] = settings.contrast;
coeffs = dot(coeffs, m);

// Apply saturation
coeffs = dot(coeffs, saturation_matrix(settings.saturation));
Expand Down
33 changes: 18 additions & 15 deletions application/src/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,18 @@ Result draw_color_tab(Config &ctx) {
// Temperature sliders
im::SeparatorText("Temperature");
auto max_temp = enable_extra_hot_temps ? MAX_TEMP : D65_TEMP;
ctx.is_editing_day_profile |= new_slider("Day:", "##tempd", ctx.profile.day_settings.temperature, MIN_TEMP, max_temp, "%d°K");
ctx.is_editing_day_profile |= new_slider("Day:", "##tempd", ctx.profile.day_settings .temperature, MIN_TEMP, max_temp, "%d°K");
ctx.is_editing_night_profile |= new_slider("Night:", "##tempn", ctx.profile.night_settings.temperature, MIN_TEMP, max_temp, "%d°K");
im::Checkbox("Enable blue temperatures", &enable_extra_hot_temps);

// Saturation sliders
im::SeparatorText("Saturation");
ctx.is_editing_day_profile |= new_slider("Day:", "##satd", ctx.profile.day_settings.saturation, MIN_SAT, MAX_SAT, "%.2f");
ctx.is_editing_day_profile |= new_slider("Day:", "##satd", ctx.profile.day_settings .saturation, MIN_SAT, MAX_SAT, "%.2f");
ctx.is_editing_night_profile |= new_slider("Night:", "##satn", ctx.profile.night_settings.saturation, MIN_SAT, MAX_SAT, "%.2f");

// Hue sliders
im::SeparatorText("Hue");
ctx.is_editing_day_profile |= new_slider("Day:", "##hued", ctx.profile.day_settings.hue, MIN_HUE, MAX_HUE, "%.2f");
ctx.is_editing_day_profile |= new_slider("Day:", "##hued", ctx.profile.day_settings .hue, MIN_HUE, MAX_HUE, "%.2f");
ctx.is_editing_night_profile |= new_slider("Night:", "##huen", ctx.profile.night_settings.hue, MIN_HUE, MAX_HUE, "%.2f");

// Components checkboxes
Expand Down Expand Up @@ -251,19 +251,24 @@ Result draw_correction_tab(Config &ctx) {
if (!im::BeginTabItem("Correction"))
return 0;

// Contrast sliders
im::SeparatorText("Contrast");
ctx.is_editing_day_profile |= new_slider("Day:", "##contrastd", ctx.profile.day_settings .contrast, MIN_CONTRAST, MAX_CONTRAST, "%.2f");
ctx.is_editing_night_profile |= new_slider("Night:", "##contrastn", ctx.profile.night_settings.contrast, MIN_CONTRAST, MAX_CONTRAST, "%.2f");

// Gamma sliders
im::SeparatorText("Gamma");
ctx.is_editing_day_profile |= new_slider("Day:", "##gammad", ctx.profile.day_settings.gamma, MIN_GAMMA, MAX_GAMMA, "%.2f");
ctx.is_editing_day_profile |= new_slider("Day:", "##gammad", ctx.profile.day_settings .gamma, MIN_GAMMA, MAX_GAMMA, "%.2f");
ctx.is_editing_night_profile |= new_slider("Night:", "##gamman", ctx.profile.night_settings.gamma, MIN_GAMMA, MAX_GAMMA, "%.2f");

// Luminance sliders
im::SeparatorText("Luminance");
ctx.is_editing_day_profile |= new_slider("Day:", "##lumad", ctx.profile.day_settings.luminance, MIN_LUMA, MAX_LUMA, "%.2f", true);
ctx.is_editing_day_profile |= new_slider("Day:", "##lumad", ctx.profile.day_settings .luminance, MIN_LUMA, MAX_LUMA, "%.2f", true);
ctx.is_editing_night_profile |= new_slider("Night:", "##luman", ctx.profile.night_settings.luminance, MIN_LUMA, MAX_LUMA, "%.2f", true);

// Color range sliders
im::SeparatorText("Color range");
ctx.is_editing_day_profile |= new_range("Day:", "Full range##d", "##rangeld", "##ranghd", ctx.profile.day_settings.range);
ctx.is_editing_day_profile |= new_range("Day:", "Full range##d", "##rangeld", "##ranghd", ctx.profile.day_settings .range);
ctx.is_editing_night_profile |= new_range("Night:", "Full range##n", "##rangeln", "##ranghn", ctx.profile.night_settings.range);

im::EndTabItem();
Expand Down Expand Up @@ -469,20 +474,18 @@ void draw_graph_window(Config &ctx) {
im::SetWindowPos( { 0.53f * width, 0.60f * height }, ImGuiCond_Always);
im::SetWindowSize({ 0.38f * width, 0.35f * height }, ImGuiCond_Always);

Gamma gamma = DEFAULT_GAMMA; Luminance luma = DEFAULT_LUMA; ColorRange range = DEFAULT_RANGE;
if (ctx.is_editing_day_profile)
gamma = ctx.profile.day_settings.gamma, luma = ctx.profile.day_settings.luminance, range = ctx.profile.day_settings.range;
else if (ctx.is_editing_night_profile)
gamma = ctx.profile.night_settings.gamma, luma = ctx.profile.night_settings.luminance, range = ctx.profile.night_settings.range;
FizeauSettings set = ctx.is_editing_day_profile ? ctx.profile.day_settings : ctx.profile.night_settings;

// Calculate ramps
std::array<std::uint16_t, 256> lut1;
degamma_ramp(lut1.data(), lut1.size(), DEFAULT_GAMMA, 8);
std::array<std::uint16_t, 960> lut2;
regamma_ramp(lut2.data(), lut2.size(), gamma, 8);

apply_luma(lut2.data(), lut2.size(), luma);
apply_range(lut2.data(), lut2.size(), range.lo, std::min(range.hi, lut2.back() / 255.0f));
float off = (1.0f - set.contrast) / 2.0f;
degamma_ramp(lut1.data(), lut1.size(), DEFAULT_GAMMA, 8);
regamma_ramp(lut2.data(), lut2.size(), set.gamma, 8, 0.0f, 1.0f, off);

apply_luma(lut2.data(), lut2.size(), 8, set.luminance);
apply_range(lut2.data(), lut2.size(), 8, set.range.lo, std::min(set.range.hi, lut2.back() / 255.0f));

std::array<float, 2> linear = { 0, 1 };
std::array<float, lut1.size()> lut1_float;
Expand Down
14 changes: 7 additions & 7 deletions common/include/color.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ ColorMatrix saturation_matrix(Saturation sat);
float degamma(float x, Gamma gamma);
float regamma(float x, Gamma gamma);

void gamma_ramp(float (*func)(float, Gamma), std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo, float hi);
void gamma_ramp(float (*func)(float, Gamma), std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo, float hi, float off);

[[maybe_unused]]
static inline void degamma_ramp(std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo = 0.0f, float hi = 1.0f) {
return gamma_ramp(degamma, array, size, gamma, nb_bits, lo, hi);
static inline void degamma_ramp(std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo = 0.0f, float hi = 1.0f, float off = 0.0f) {
return gamma_ramp(degamma, array, size, gamma, nb_bits, lo, hi, off);
}

[[maybe_unused]]
static inline void regamma_ramp(std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo = 0.0f, float hi = 1.0f) {
return gamma_ramp(regamma, array, size, gamma, nb_bits, lo, hi);
static inline void regamma_ramp(std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo = 0.0f, float hi = 1.0f, float off = 0.0f) {
return gamma_ramp(regamma, array, size, gamma, nb_bits, lo, hi, off);
}

void apply_luma(std::uint16_t *array, std::size_t size, Luminance luma);
void apply_range(std::uint16_t *array, std::size_t size, float lo, float hi);
void apply_luma(std::uint16_t *array, std::size_t size, std::size_t nb_bits, Luminance luma);
void apply_range(std::uint16_t *array, std::size_t size, std::size_t nb_bits, float lo, float hi);

} // namespace fz
1 change: 1 addition & 0 deletions common/include/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Config {
.temperature = DEFAULT_TEMP,
.saturation = DEFAULT_SAT,
.hue = DEFAULT_HUE,
.contrast = DEFAULT_CONTRAST,
.gamma = DEFAULT_GAMMA,
.luminance = DEFAULT_LUMA,
.range = DEFAULT_RANGE,
Expand Down
1 change: 1 addition & 0 deletions common/include/fizeau.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ typedef struct {
Temperature temperature;
Saturation saturation;
Hue hue;
Contrast contrast;
Gamma gamma;
Luminance luminance;
ColorRange range;
Expand Down
25 changes: 15 additions & 10 deletions common/include/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ typedef uint32_t Temperature;
#define MAX_TEMP 10000u // °K
#define DEFAULT_TEMP D65_TEMP

typedef float Saturation;
#define MIN_SAT 0.0f
#define MAX_SAT 2.0f
#define DEFAULT_SAT 1.0f

typedef float Hue;
#define MIN_HUE -1.0f
#define MAX_HUE 1.0f
#define DEFAULT_HUE 0.0f

typedef enum {
Component_None = 0,
Component_Red = BIT(0),
Expand All @@ -37,6 +47,11 @@ typedef enum {
Component_All = Component_Red | Component_Green | Component_Blue,
} Component;

typedef float Contrast;
#define MIN_CONTRAST 0.0f
#define MAX_CONTRAST 2.0f
#define DEFAULT_CONTRAST 1.0f

typedef float Gamma;
#define MIN_GAMMA 0.0f
#define MAX_GAMMA 5.0f
Expand All @@ -47,16 +62,6 @@ typedef float Luminance;
#define MAX_LUMA 1.0f
#define DEFAULT_LUMA 0.0f

typedef float Hue;
#define MIN_HUE -1.0f
#define MAX_HUE 1.0f
#define DEFAULT_HUE 0.0f

typedef float Saturation;
#define MIN_SAT 0.0f
#define MAX_SAT 2.0f
#define DEFAULT_SAT 1.0f

typedef struct {
float lo, hi;
} ColorRange;
Expand Down
12 changes: 6 additions & 6 deletions common/src/color.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,31 +120,31 @@ float regamma(float x, Gamma gamma) {

}

void gamma_ramp(float (*func)(float, Gamma), std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo, float hi) {
void gamma_ramp(float (*func)(float, Gamma), std::uint16_t *array, std::size_t size, Gamma gamma, std::size_t nb_bits, float lo, float hi, float off) {
float step = (hi - lo) / (size - 1), cur = lo;
std::uint16_t shift = (1 << nb_bits) - 1, mask = (1 << (nb_bits + 1)) - 1;

for (std::size_t i = 0; i < size; ++i, cur += step)
array[i] = static_cast<std::uint16_t>(std::round(func(cur, gamma) * shift)) & mask;
array[i] = static_cast<std::uint16_t>(std::round(func(std::clamp(cur + off, 0.0f, 1.0f), gamma) * shift)) & mask;
}

void apply_luma(std::uint16_t *array, std::size_t size, Luminance luma) {
void apply_luma(std::uint16_t *array, std::size_t size, std::size_t nb_bits, Luminance luma) {
luma = std::clamp(luma, MIN_LUMA, MAX_LUMA) + MAX_LUMA;
if (luma == 1.0f)
return; // No effect, fast path

auto max = array[size - 1];
auto max = (1 << nb_bits) - 1;
for (std::size_t i = 0; i < size; ++i)
array[i] = std::clamp(static_cast<std::uint16_t>(std::round(array[i] * luma)),
static_cast<std::uint16_t>(0), static_cast<std::uint16_t>(max));
}

void apply_range(std::uint16_t *array, std::size_t size, float lo, float hi) {
void apply_range(std::uint16_t *array, std::size_t size, std::size_t nb_bits, float lo, float hi) {
lo = std::clamp(lo, 0.0f, hi), hi = std::clamp(hi, lo, 1.0f);
if ((lo == 0.0f) && (hi == 1.0f))
return; // No effect, fast path

auto max = array[size - 1];
auto max = std::min<std::size_t>(array[size - 1], (1 << nb_bits) - 1);
for (std::size_t i = 0; i < size; ++i)
array[i] = static_cast<std::uint16_t>(std::round(array[i] * (hi - lo) + lo * max));
}
Expand Down
27 changes: 17 additions & 10 deletions common/src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,26 @@ void Config::sanitize_profile() {
sanitize_time(this->profile.dawn_begin);
sanitize_time(this->profile.dawn_end);

sanitize_minmax(this->profile.day_settings .temperature, MIN_TEMP, MAX_TEMP);
sanitize_minmax(this->profile.night_settings.temperature, MIN_TEMP, MAX_TEMP);
sanitize_minmax(this->profile.day_settings .temperature, MIN_TEMP, MAX_TEMP);
sanitize_minmax(this->profile.night_settings.temperature, MIN_TEMP, MAX_TEMP);

sanitize_minmax(this->profile.day_settings .saturation, MIN_SAT, MAX_SAT);
sanitize_minmax(this->profile.night_settings.saturation, MIN_SAT, MAX_SAT);
sanitize_minmax(this->profile.day_settings .saturation, MIN_SAT, MAX_SAT);
sanitize_minmax(this->profile.night_settings.saturation, MIN_SAT, MAX_SAT);

sanitize_minmax(this->profile.day_settings .hue, MIN_HUE, MAX_HUE);
sanitize_minmax(this->profile.night_settings.hue, MIN_HUE, MAX_HUE);
sanitize_minmax(this->profile.day_settings .hue, MIN_HUE, MAX_HUE);
sanitize_minmax(this->profile.night_settings.hue, MIN_HUE, MAX_HUE);

sanitize_minmax(this->profile.components, Component_None, Component_All);
sanitize_filter(this->profile.filter);

sanitize_minmax(this->profile.day_settings .gamma, MIN_GAMMA, MAX_GAMMA);
sanitize_minmax(this->profile.night_settings.gamma, MIN_GAMMA, MAX_GAMMA);
sanitize_minmax(this->profile.day_settings .contrast, MIN_CONTRAST, MAX_CONTRAST);
sanitize_minmax(this->profile.night_settings.contrast, MIN_CONTRAST, MAX_CONTRAST);

sanitize_minmax(this->profile.day_settings .luminance, MIN_LUMA, MAX_LUMA);
sanitize_minmax(this->profile.night_settings.luminance, MIN_LUMA, MAX_LUMA);
sanitize_minmax(this->profile.day_settings .gamma, MIN_GAMMA, MAX_GAMMA);
sanitize_minmax(this->profile.night_settings.gamma, MIN_GAMMA, MAX_GAMMA);

sanitize_minmax(this->profile.day_settings .luminance, MIN_LUMA, MAX_LUMA);
sanitize_minmax(this->profile.night_settings.luminance, MIN_LUMA, MAX_LUMA);

sanitize_colorrange(this->profile.day_settings .range);
sanitize_colorrange(this->profile.night_settings.range);
Expand Down Expand Up @@ -190,6 +193,9 @@ std::string Config::make() {

str += "filter = " + format_filter(this->profile.filter) + '\n';

str += "contrast_day = " + std::to_string(this->profile.day_settings .contrast) + '\n';
str += "contrast_night = " + std::to_string(this->profile.night_settings.contrast) + '\n';

str += "gamma_day = " + std::to_string(this->profile.day_settings .gamma) + '\n';
str += "gamma_night = " + std::to_string(this->profile.night_settings.gamma) + '\n';

Expand Down Expand Up @@ -243,6 +249,7 @@ Result Config::reset() {
this->profile.day_settings.temperature = DEFAULT_TEMP, this->profile.night_settings.temperature = DEFAULT_TEMP;
this->profile.day_settings.saturation = DEFAULT_SAT, this->profile.night_settings.saturation = DEFAULT_SAT;
this->profile.day_settings.hue = DEFAULT_HUE, this->profile.night_settings.hue = DEFAULT_HUE;
this->profile.day_settings.contrast = DEFAULT_CONTRAST, this->profile.night_settings.contrast = DEFAULT_CONTRAST;
this->profile.day_settings.gamma = DEFAULT_GAMMA, this->profile.night_settings.gamma = DEFAULT_GAMMA;
this->profile.day_settings.luminance = DEFAULT_LUMA, this->profile.night_settings.luminance = DEFAULT_LUMA;
this->profile.day_settings.range = DEFAULT_RANGE, this->profile.night_settings.range = DEFAULT_RANGE;
Expand Down
2 changes: 2 additions & 0 deletions common/src/config_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ int Config::ini_handler(void *user, const char *section, const char *name, const
MATCH_SET(name, "saturation_night", p.night_settings.saturation) ||
MATCH_SET(name, "hue_day", p.day_settings .hue) ||
MATCH_SET(name, "hue_night", p.night_settings.hue) ||
MATCH_SET(name, "contrast_day", p.day_settings .contrast) ||
MATCH_SET(name, "contrast_night", p.night_settings.contrast) ||
MATCH_SET(name, "gamma_day", p.day_settings .gamma) ||
MATCH_SET(name, "gamma_night", p.night_settings.gamma) ||
MATCH_SET(name, "luminance_day", p.day_settings .luminance) ||
Expand Down
9 changes: 8 additions & 1 deletion misc/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ components = all
; Value has to be "red", "green", "blue" or "none"
filter = none

; Gamma (similar to contrast)
; Contrast
; Value has to be >=0.0, and <=2.0
contrast_day = 1.0
contrast_night = 1.0

; Gamma
; Value has to be >=0.0, and <=5.0
gamma_day = 2.4
gamma_night = 2.4
Expand Down Expand Up @@ -69,6 +74,8 @@ hue_day = 0.0
hue_night = 0.0
filter = none
components = all
contrast_day = 1.0
contrast_night = 1.0
gamma_day = 2.4
gamma_night = 2.4
luminance_day = 0.0
Expand Down
Loading

0 comments on commit 03d4c3b

Please sign in to comment.