aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-07-14 21:07:04 +0200
committerdec05eba <dec05eba@protonmail.com>2026-07-14 21:07:04 +0200
commitbcd9f4c69103d766e3dcd8ffa10b5880ea5110bf (patch)
tree27f3ae08223971a52c9e3598336b3601b40e71fa
parent7a5785e4dfd023dc5e037ad535f5b7aa26278ff4 (diff)
kms capture: make capture work with night lights enabled
-rw-r--r--README.md3
-rw-r--r--include/color_conversion.h7
-rw-r--r--include/egl.h1
-rw-r--r--include/kde_night_light.h13
-rw-r--r--meson.build2
-rw-r--r--project.conf1
-rw-r--r--src/capture/kms.c28
-rw-r--r--src/color_conversion.c56
-rw-r--r--src/egl.c1
-rw-r--r--src/kde_night_light.c211
10 files changed, 315 insertions, 8 deletions
diff --git a/README.md b/README.md
index c63603a..a21908b 100644
--- a/README.md
+++ b/README.md
@@ -237,7 +237,8 @@ This can happen if your video player is missing the H264/HEVC video codecs. Eith
## I get stutter in the video
Try recording to an SSD and make sure it's not using NTFS file system. Also try recording with "content" framerate mode (`-fm content`).
## GPU Screen Recorder records night light
-You can record with desktop portal option (`-w portal`) instead which ignores night light, if you are ok with recording without HDR.
+On KDE Plasma the night light tint is automatically removed from the recording (both in SDR and HDR mode).
+On other compositors you can record with desktop portal option (`-w portal`) instead which ignores night light, if you are ok with recording without HDR.
## Kdenlive says that the video is not usable for editing because it has variable frame rate
To fix this you can either just press cancel, which will allow you to continue or record the video in .mkv format or constant frame rate (-fm cfr). I recommend recording the video in .mkv format and variable frame rate (-fm vfr).
## GPU Screen Recorder starts lagging after 30-40 minutes when launching GPU Screen Recorder from steam command launcher
diff --git a/include/color_conversion.h b/include/color_conversion.h
index 7950472..251fe17 100644
--- a/include/color_conversion.h
+++ b/include/color_conversion.h
@@ -41,6 +41,8 @@ typedef struct {
int hdr_sdr_white_pq;
int hdr_sdr_white_scale;
int gamma_lut_enabled;
+ int night_light_transfer;
+ int night_light_matrix;
} gsr_color_graphics_uniforms;
typedef struct {
@@ -73,6 +75,8 @@ typedef struct {
float hdr_sdr_white_scale;
unsigned int gamma_lut_texture_id;
bool gamma_lut_apply;
+ int night_light_transfer;
+ float night_light_matrix[9];
bool hdr_shaders_loaded;
bool hdr_shaders_load_failed;
} gsr_color_conversion;
@@ -85,6 +89,9 @@ void gsr_color_conversion_clear(gsr_color_conversion *self);
void gsr_color_conversion_set_hdr_to_sdr_tone_mapping(gsr_color_conversion *self, bool enable, float hdr_peak_luminance, float sdr_white_luminance);
void gsr_color_conversion_set_gamma_lut(gsr_color_conversion *self, const float *rgb_values, int num_entries);
void gsr_color_conversion_enable_gamma_lut(gsr_color_conversion *self, bool enable);
+/* |inverse_matrix| is a row major 3x3 matrix that is applied to linear rgb values in the draw functions, for RGB/BGR sources. NULL to disable.
+ |pq_color_transfer| chooses the transfer characteristics used to decode/encode the source image around the matrix multiply (pq or gamma 2.2) */
+void gsr_color_conversion_set_night_light_matrix(gsr_color_conversion *self, const float *inverse_matrix, bool pq_color_transfer);
void gsr_color_conversion_read_destination_texture(gsr_color_conversion *self, int destination_texture_index, int x, int y, int width, int height, unsigned int color_format, unsigned int data_format, void *pixels);
gsr_rotation gsr_monitor_rotation_to_rotation(gsr_monitor_rotation monitor_rotation);
diff --git a/include/egl.h b/include/egl.h
index c9c54e5..c8dcd2b 100644
--- a/include/egl.h
+++ b/include/egl.h
@@ -311,6 +311,7 @@ struct gsr_egl {
void (*glUniform1i)(int location, int v0);
void (*glUniform2i)(int location, int v0, int v1);
void (*glUniformMatrix2fv)(int location, int count, unsigned char transpose, const float *value);
+ void (*glUniformMatrix3fv)(int location, int count, unsigned char transpose, const float *value);
void (*glDebugMessageCallback)(GLDEBUGPROC callback, const void *userParam);
void (*glScissor)(int x, int y, int width, int height);
void (*glCreateBuffers)(int n, unsigned int *buffers);
diff --git a/include/kde_night_light.h b/include/kde_night_light.h
new file mode 100644
index 0000000..ed12f1d
--- /dev/null
+++ b/include/kde_night_light.h
@@ -0,0 +1,13 @@
+#ifndef GSR_KDE_NIGHT_LIGHT_H
+#define GSR_KDE_NIGHT_LIGHT_H
+
+#include <stdbool.h>
+
+typedef struct gsr_kde_night_light gsr_kde_night_light;
+
+gsr_kde_night_light* gsr_kde_night_light_create(void);
+void gsr_kde_night_light_destroy(gsr_kde_night_light *self);
+/* Returns true when night light is active. |inverse_matrix| is filled with the row major 3x3 matrix that removes the night light tint from linear rgb values */
+bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, float inverse_matrix[9]);
+
+#endif /* GSR_KDE_NIGHT_LIGHT_H */
diff --git a/meson.build b/meson.build
index f9fbdd4..331ad27 100644
--- a/meson.build
+++ b/meson.build
@@ -41,6 +41,7 @@ src = [
'src/damage.c',
'src/image_writer.c',
'src/args_parser.c',
+ 'src/kde_night_light.c',
'src/defs.c',
'src/plugins.c',
'src/wayland_host_bridge.c',
@@ -107,6 +108,7 @@ if uses_pipewire == true
dependency('libspa-0.2'),
dependency('dbus-1'),
]
+ add_project_arguments('-DGSR_DBUS', language : ['c', 'cpp'])
endif
add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
diff --git a/project.conf b/project.conf
index 9c1f480..ca47939 100644
--- a/project.conf
+++ b/project.conf
@@ -14,6 +14,7 @@ ignore_dirs = ["kms/server", "build", "debug-build", "plugin/examples"]
[define]
GSR_PORTAL = "1"
GSR_APP_AUDIO = "1"
+GSR_DBUS = "1"
[dependencies]
libavcodec = ">=58"
diff --git a/src/capture/kms.c b/src/capture/kms.c
index 039609b..c732993 100644
--- a/src/capture/kms.c
+++ b/src/capture/kms.c
@@ -2,6 +2,7 @@
#include "../../include/utils.h"
#include "../../include/color_conversion.h"
#include "../../include/cursor.h"
+#include "../../include/kde_night_light.h"
#include "../../include/window/window.h"
#include <stdlib.h>
@@ -56,6 +57,9 @@ typedef struct {
uint32_t gamma_lut_property_id;
uint64_t gamma_lut_blob_id;
+ gsr_kde_night_light *kde_night_light;
+ bool night_light_message_shown;
+
bool is_x11;
//int drm_fd;
@@ -94,6 +98,11 @@ static void gsr_capture_kms_stop(gsr_capture_kms *self) {
self->drm_card_fd = -1;
}
+ if(self->kde_night_light) {
+ gsr_kde_night_light_destroy(self->kde_night_light);
+ self->kde_night_light = NULL;
+ }
+
// if(self->drm_fd > 0) {
// close(self->drm_fd);
// self->drm_fd = -1;
@@ -172,6 +181,8 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture
self->is_x11 = gsr_window_get_display_server(self->params.egl->window) == GSR_DISPLAY_SERVER_X11;
const gsr_connection_type connection_type = self->is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM;
+ if(!self->is_x11)
+ self->kde_night_light = gsr_kde_night_light_create();
MonitorCallbackUserdata monitor_callback_userdata = {
&self->monitor_id,
@@ -388,9 +399,25 @@ static void gsr_capture_kms_update_gamma_lut(gsr_capture_kms *self, gsr_color_co
drmModeFreePropertyBlob(blob);
}
+static void gsr_capture_kms_update_night_light(gsr_capture_kms *self, gsr_color_conversion *color_conversion, bool plane_is_hdr) {
+ float night_light_matrix[9];
+ if(!self->kde_night_light || !gsr_kde_night_light_get_inverse_matrix(self->kde_night_light, night_light_matrix)) {
+ gsr_color_conversion_set_night_light_matrix(color_conversion, NULL, false);
+ return;
+ }
+
+ gsr_color_conversion_set_night_light_matrix(color_conversion, night_light_matrix, plane_is_hdr);
+
+ if(!self->night_light_message_shown) {
+ self->night_light_message_shown = true;
+ fprintf(stderr, "gsr info: gsr_capture_kms_update_night_light: night light is active, removing the night light tint from the capture\n");
+ }
+}
+
static void gsr_capture_kms_update_hdr_to_sdr_tone_mapping(gsr_capture_kms *self, gsr_color_conversion *color_conversion, const gsr_kms_response_item *drm_fd) {
const bool plane_is_hdr = drm_plane_is_hdr(drm_fd);
gsr_color_conversion_enable_gamma_lut(color_conversion, plane_is_hdr && self->gamma_lut_blob_id != 0);
+ gsr_capture_kms_update_night_light(self, color_conversion, plane_is_hdr);
const bool tone_map_hdr_to_sdr = !self->params.hdr && plane_is_hdr;
if(!tone_map_hdr_to_sdr) {
@@ -902,6 +929,7 @@ static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *captu
gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, false, 0.0f, 0.0f);
gsr_color_conversion_enable_gamma_lut(color_conversion, false);
+ gsr_color_conversion_set_night_light_matrix(color_conversion, NULL, false);
//self->params.egl->glFlush();
//self->params.egl->glFinish();
diff --git a/src/color_conversion.c b/src/color_conversion.c
index 126cc48..6ce17d3 100644
--- a/src/color_conversion.c
+++ b/src/color_conversion.c
@@ -92,6 +92,10 @@
" } \n" \
" return e1 * hdr_source_max_pq; \n" \
"} \n" \
+ "vec3 luminance3_to_pq(vec3 luminance) { \n" \
+ " vec3 l = pow(clamp(luminance, vec3(0.0), vec3(1.0)), vec3(PQ_M1)); \n" \
+ " return pow((PQ_C1 + PQ_C2*l) / (1.0 + PQ_C3*l), vec3(PQ_M2)); \n" \
+ "} \n" \
"vec3 tone_map_hdr_to_sdr(vec3 pq_rgb) { \n" \
" vec3 luminance = pq_to_luminance3(pq_rgb); \n" \
" float max_channel = max(luminance.r, max(luminance.g, luminance.b)); \n" \
@@ -101,6 +105,20 @@
" return pow(clamp(luminance * hdr_sdr_white_scale, 0.0, 1.0), vec3(1.0/2.2)); \n" \
"} \n"
+#define NIGHT_LIGHT_GLSL \
+ "uniform int night_light_transfer; \n" \
+ "uniform mat3 night_light_matrix; \n" \
+ "vec3 remove_night_light(vec3 color) { \n" \
+ " if(night_light_transfer == 1) { \n" \
+ " vec3 luminance = max(night_light_matrix * pq_to_luminance3(color), vec3(0.0)); \n" \
+ " return luminance3_to_pq(luminance); \n" \
+ " } else { \n" \
+ " vec3 linear_color = pow(max(color, vec3(0.0)), vec3(2.2)); \n" \
+ " linear_color = clamp(night_light_matrix * linear_color, vec3(0.0), vec3(1.0)); \n" \
+ " return pow(linear_color, vec3(1.0/2.2)); \n" \
+ " } \n" \
+ "} \n"
+
#define GAMMA_LUT_GLSL \
"uniform sampler2D gamma_lut; \n" \
"uniform float gamma_lut_enabled; \n" \
@@ -119,6 +137,8 @@
#define APPLY_HDR_TO_SDR_TONE_MAPPING \
" if(gamma_lut_enabled > 0.5) \n" \
" pixel.rgb = apply_gamma_lut(pixel.rgb); \n" \
+ " if(night_light_transfer != 0) \n" \
+ " pixel.rgb = remove_night_light(pixel.rgb); \n" \
" if(hdr_source_max_pq > 0.0) \n" \
" pixel.rgb = tone_map_hdr_to_sdr(pixel.rgb); \n"
@@ -191,7 +211,7 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -204,7 +224,7 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -218,6 +238,8 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
uniforms->hdr_sdr_white_pq = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_pq");
uniforms->hdr_sdr_white_scale = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_scale");
uniforms->gamma_lut_enabled = egl->glGetUniformLocation(shader->program_id, "gamma_lut_enabled");
+ uniforms->night_light_transfer = egl->glGetUniformLocation(shader->program_id, "night_light_transfer");
+ uniforms->night_light_matrix = egl->glGetUniformLocation(shader->program_id, "night_light_matrix");
egl->glUseProgram(shader->program_id);
egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
@@ -265,7 +287,7 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -278,7 +300,7 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -292,6 +314,8 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
uniforms->hdr_sdr_white_pq = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_pq");
uniforms->hdr_sdr_white_scale = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_scale");
uniforms->gamma_lut_enabled = egl->glGetUniformLocation(shader->program_id, "gamma_lut_enabled");
+ uniforms->night_light_transfer = egl->glGetUniformLocation(shader->program_id, "night_light_transfer");
+ uniforms->night_light_matrix = egl->glGetUniformLocation(shader->program_id, "night_light_matrix");
egl->glUseProgram(shader->program_id);
egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
@@ -335,7 +359,7 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
"void main() \n"
"{ \n"
"%s"
- "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -347,7 +371,7 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
"void main() \n"
"{ \n"
"%s"
- "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
+ "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING NIGHT_LIGHT_GLSL GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -361,6 +385,8 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
uniforms->hdr_sdr_white_pq = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_pq");
uniforms->hdr_sdr_white_scale = egl->glGetUniformLocation(shader->program_id, "hdr_sdr_white_scale");
uniforms->gamma_lut_enabled = egl->glGetUniformLocation(shader->program_id, "gamma_lut_enabled");
+ uniforms->night_light_transfer = egl->glGetUniformLocation(shader->program_id, "night_light_transfer");
+ uniforms->night_light_matrix = egl->glGetUniformLocation(shader->program_id, "night_light_matrix");
egl->glUseProgram(shader->program_id);
egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
@@ -632,7 +658,7 @@ static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion
}
static bool gsr_color_conversion_load_hdr_graphics_shaders_if_needed(gsr_color_conversion *self) {
- const bool hdr_enabled = self->hdr_source_max_pq > 0.0f || (self->gamma_lut_apply && self->gamma_lut_texture_id != 0);
+ const bool hdr_enabled = self->hdr_source_max_pq > 0.0f || (self->gamma_lut_apply && self->gamma_lut_texture_id != 0) || self->night_light_transfer != 0;
if(!hdr_enabled || self->hdr_shaders_load_failed)
return false;
@@ -1004,6 +1030,8 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_pq, self->hdr_sdr_white_pq);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_scale, self->hdr_sdr_white_scale);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].gamma_lut_enabled, gamma_lut_enabled);
+ self->params.egl->glUniform1i(self->graphics_uniforms[shader_index].night_light_transfer, self->night_light_transfer);
+ self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].night_light_matrix, 1, GL_TRUE, self->night_light_matrix);
}
self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
@@ -1023,6 +1051,8 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_pq, self->hdr_sdr_white_pq);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_scale, self->hdr_sdr_white_scale);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].gamma_lut_enabled, gamma_lut_enabled);
+ self->params.egl->glUniform1i(self->graphics_uniforms[shader_index].night_light_transfer, self->night_light_transfer);
+ self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].night_light_matrix, 1, GL_TRUE, self->night_light_matrix);
}
self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
}
@@ -1045,6 +1075,8 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_pq, self->hdr_sdr_white_pq);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_sdr_white_scale, self->hdr_sdr_white_scale);
self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].gamma_lut_enabled, gamma_lut_enabled);
+ self->params.egl->glUniform1i(self->graphics_uniforms[shader_index].night_light_transfer, self->night_light_transfer);
+ self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].night_light_matrix, 1, GL_TRUE, self->night_light_matrix);
}
self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
break;
@@ -1225,6 +1257,16 @@ void gsr_color_conversion_enable_gamma_lut(gsr_color_conversion *self, bool enab
self->gamma_lut_apply = enable;
}
+void gsr_color_conversion_set_night_light_matrix(gsr_color_conversion *self, const float *inverse_matrix, bool pq_color_transfer) {
+ if(!inverse_matrix) {
+ self->night_light_transfer = 0;
+ return;
+ }
+
+ memcpy(self->night_light_matrix, inverse_matrix, sizeof(self->night_light_matrix));
+ self->night_light_transfer = pq_color_transfer ? 1 : 2;
+}
+
void gsr_color_conversion_read_destination_texture(gsr_color_conversion *self, int destination_texture_index, int x, int y, int width, int height, unsigned int color_format, unsigned int data_format, void *pixels) {
assert(destination_texture_index >= 0 && destination_texture_index < self->params.num_destination_textures);
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[destination_texture_index]);
diff --git a/src/egl.c b/src/egl.c
index d3b89cc..3dcd5df 100644
--- a/src/egl.c
+++ b/src/egl.c
@@ -331,6 +331,7 @@ static bool gsr_egl_load_gl(gsr_egl *self, void *library) {
{ (void**)&self->glUniform1i, "glUniform1i" },
{ (void**)&self->glUniform2i, "glUniform2i" },
{ (void**)&self->glUniformMatrix2fv, "glUniformMatrix2fv" },
+ { (void**)&self->glUniformMatrix3fv, "glUniformMatrix3fv" },
{ (void**)&self->glDebugMessageCallback, "glDebugMessageCallback" },
{ (void**)&self->glScissor, "glScissor" },
{ (void**)&self->glReadPixels, "glReadPixels" },
diff --git a/src/kde_night_light.c b/src/kde_night_light.c
new file mode 100644
index 0000000..859a56f
--- /dev/null
+++ b/src/kde_night_light.c
@@ -0,0 +1,211 @@
+#include "../include/kde_night_light.h"
+
+#ifdef GSR_DBUS
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <dbus/dbus.h>
+
+#define NIGHT_LIGHT_POLL_SECONDS 1
+#define NIGHT_LIGHT_TEMPERATURE_NEUTRAL 6500
+#define NIGHT_LIGHT_TEMPERATURE_MIN 1000
+
+typedef struct {
+ double v[3];
+} vec3d;
+
+struct gsr_kde_night_light {
+ pthread_t thread;
+ bool thread_created;
+ atomic_bool stop;
+ atomic_int temperature;
+ int cached_temperature;
+ float cached_inverse_matrix[9];
+};
+
+/* Blackbody whitepoint table for 1000K-6500K at 100K intervals, from https://github.com/jonls/redshift/blob/master/README-colorramp */
+static const double blackbody_whitepoints[56][3] = {
+ {1.00000000, 0.18172716, 0.00000000}, {1.00000000, 0.25503671, 0.00000000},
+ {1.00000000, 0.30942099, 0.00000000}, {1.00000000, 0.35357379, 0.00000000},
+ {1.00000000, 0.39091524, 0.00000000}, {1.00000000, 0.42322816, 0.00000000},
+ {1.00000000, 0.45159884, 0.00000000}, {1.00000000, 0.47675916, 0.00000000},
+ {1.00000000, 0.49923747, 0.00000000}, {1.00000000, 0.51943421, 0.00000000},
+ {1.00000000, 0.54360078, 0.08679949}, {1.00000000, 0.56618736, 0.14065513},
+ {1.00000000, 0.58734976, 0.18362641}, {1.00000000, 0.60724493, 0.22137978},
+ {1.00000000, 0.62600248, 0.25591950}, {1.00000000, 0.64373109, 0.28819679},
+ {1.00000000, 0.66052319, 0.31873863}, {1.00000000, 0.67645822, 0.34786758},
+ {1.00000000, 0.69160518, 0.37579588}, {1.00000000, 0.70602449, 0.40267128},
+ {1.00000000, 0.71976951, 0.42860152}, {1.00000000, 0.73288760, 0.45366838},
+ {1.00000000, 0.74542112, 0.47793608}, {1.00000000, 0.75740814, 0.50145662},
+ {1.00000000, 0.76888303, 0.52427322}, {1.00000000, 0.77987699, 0.54642268},
+ {1.00000000, 0.79041843, 0.56793692}, {1.00000000, 0.80053332, 0.58884417},
+ {1.00000000, 0.81024551, 0.60916971}, {1.00000000, 0.81957693, 0.62893653},
+ {1.00000000, 0.82854786, 0.64816570}, {1.00000000, 0.83717703, 0.66687674},
+ {1.00000000, 0.84548188, 0.68508786}, {1.00000000, 0.85347859, 0.70281616},
+ {1.00000000, 0.86118227, 0.72007777}, {1.00000000, 0.86860704, 0.73688797},
+ {1.00000000, 0.87576611, 0.75326132}, {1.00000000, 0.88267187, 0.76921169},
+ {1.00000000, 0.88933596, 0.78475236}, {1.00000000, 0.89576933, 0.79989606},
+ {1.00000000, 0.90198230, 0.81465502}, {1.00000000, 0.90963069, 0.82838210},
+ {1.00000000, 0.91710889, 0.84190889}, {1.00000000, 0.92441842, 0.85523742},
+ {1.00000000, 0.93156127, 0.86836903}, {1.00000000, 0.93853986, 0.88130458},
+ {1.00000000, 0.94535695, 0.89404470}, {1.00000000, 0.95201559, 0.90658983},
+ {1.00000000, 0.95851906, 0.91894041}, {1.00000000, 0.96487079, 0.93109690},
+ {1.00000000, 0.97107439, 0.94305985}, {1.00000000, 0.97713351, 0.95482993},
+ {1.00000000, 0.98305189, 0.96640795}, {1.00000000, 0.98883326, 0.97779486},
+ {1.00000000, 0.99448139, 0.98899179}, {1.00000000, 1.00000000, 1.00000000}
+};
+
+/* The same channel factors that kde plasma uses for night light (kwin colortemperature.h, sampleColorTemperature) */
+static vec3d night_light_channel_factors(int temperature) {
+ if(temperature < NIGHT_LIGHT_TEMPERATURE_MIN)
+ temperature = NIGHT_LIGHT_TEMPERATURE_MIN;
+ if(temperature >= NIGHT_LIGHT_TEMPERATURE_NEUTRAL)
+ return (vec3d){ .v = { 1.0, 1.0, 1.0 } };
+
+ const int index = (temperature - NIGHT_LIGHT_TEMPERATURE_MIN) / 100;
+ const double blend = (temperature % 100) / 100.0;
+ vec3d result;
+ for(int i = 0; i < 3; ++i) {
+ const double whitepoint = blackbody_whitepoints[index][i] * (1.0 - blend) + blackbody_whitepoints[index + 1][i] * blend;
+ result.v[i] = pow(whitepoint, 2.2);
+ }
+ return result;
+}
+
+/* Kde plasma multiplies the composited image with the night light channel factors in linear space (verified against kde plasma 6.7, both in sdr and hdr mode) */
+static void compute_inverse_night_light_matrix(int temperature, float inverse_matrix[9]) {
+ const vec3d factors = night_light_channel_factors(temperature);
+ memset(inverse_matrix, 0, sizeof(float) * 9);
+ inverse_matrix[0] = 1.0 / fmax(factors.v[0], 0.0001);
+ inverse_matrix[4] = 1.0 / fmax(factors.v[1], 0.0001);
+ inverse_matrix[8] = 1.0 / fmax(factors.v[2], 0.0001);
+}
+
+static bool dbus_get_night_light_property(DBusConnection *connection, const char *property_name, int expected_type, DBusBasicValue *value) {
+ DBusMessage *message = dbus_message_new_method_call("org.kde.KWin.NightLight", "/org/kde/KWin/NightLight", "org.freedesktop.DBus.Properties", "Get");
+ if(!message)
+ return false;
+
+ const char *interface_name = "org.kde.KWin.NightLight";
+ dbus_message_append_args(message, DBUS_TYPE_STRING, &interface_name, DBUS_TYPE_STRING, &property_name, DBUS_TYPE_INVALID);
+
+ DBusMessage *reply = dbus_connection_send_with_reply_and_block(connection, message, 500, NULL);
+ dbus_message_unref(message);
+ if(!reply)
+ return false;
+
+ bool success = false;
+ DBusMessageIter iter;
+ if(dbus_message_iter_init(reply, &iter) && dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_VARIANT) {
+ DBusMessageIter variant_iter;
+ dbus_message_iter_recurse(&iter, &variant_iter);
+ if(dbus_message_iter_get_arg_type(&variant_iter) == expected_type) {
+ dbus_message_iter_get_basic(&variant_iter, value);
+ success = true;
+ }
+ }
+
+ dbus_message_unref(reply);
+ return success;
+}
+
+static int query_night_light_temperature(DBusConnection *connection) {
+ DBusBasicValue temperature;
+ if(!dbus_get_night_light_property(connection, "currentTemperature", DBUS_TYPE_UINT32, &temperature))
+ return 0;
+
+ if(temperature.u32 == 0 || temperature.u32 >= NIGHT_LIGHT_TEMPERATURE_NEUTRAL)
+ return 0;
+
+ return temperature.u32;
+}
+
+static void* night_light_poll_thread(void *userdata) {
+ gsr_kde_night_light *self = userdata;
+
+ DBusConnection *connection = dbus_bus_get_private(DBUS_BUS_SESSION, NULL);
+ if(!connection) {
+ atomic_store(&self->temperature, 0);
+ return NULL;
+ }
+
+ while(!atomic_load(&self->stop)) {
+ atomic_store(&self->temperature, query_night_light_temperature(connection));
+ for(int i = 0; i < NIGHT_LIGHT_POLL_SECONDS * 10 && !atomic_load(&self->stop); ++i) {
+ usleep(100 * 1000);
+ }
+ }
+
+ dbus_connection_close(connection);
+ dbus_connection_unref(connection);
+ return NULL;
+}
+
+gsr_kde_night_light* gsr_kde_night_light_create(void) {
+ gsr_kde_night_light *self = calloc(1, sizeof(gsr_kde_night_light));
+ if(!self)
+ return NULL;
+
+ self->cached_temperature = -1;
+
+ if(pthread_create(&self->thread, NULL, night_light_poll_thread, self) != 0) {
+ free(self);
+ return NULL;
+ }
+
+ self->thread_created = true;
+ return self;
+}
+
+void gsr_kde_night_light_destroy(gsr_kde_night_light *self) {
+ if(!self)
+ return;
+
+ if(self->thread_created) {
+ atomic_store(&self->stop, true);
+ pthread_join(self->thread, NULL);
+ }
+ free(self);
+}
+
+bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, float inverse_matrix[9]) {
+ if(!self)
+ return false;
+
+ const int temperature = atomic_load(&self->temperature);
+ if(temperature <= 0)
+ return false;
+
+ if(self->cached_temperature != temperature) {
+ compute_inverse_night_light_matrix(temperature, self->cached_inverse_matrix);
+ self->cached_temperature = temperature;
+ }
+
+ memcpy(inverse_matrix, self->cached_inverse_matrix, sizeof(self->cached_inverse_matrix));
+ return true;
+}
+
+#else /* GSR_DBUS */
+
+#include <stddef.h>
+
+gsr_kde_night_light* gsr_kde_night_light_create(void) {
+ return NULL;
+}
+
+void gsr_kde_night_light_destroy(gsr_kde_night_light *self) {
+ (void)self;
+}
+
+bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, float inverse_matrix[9]) {
+ (void)self;
+ (void)inverse_matrix;
+ return false;
+}
+
+#endif /* GSR_DBUS */