aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-07-14 21:06:27 +0200
committerdec05eba <dec05eba@protonmail.com>2026-07-14 21:06:27 +0200
commit7a5785e4dfd023dc5e037ad535f5b7aa26278ff4 (patch)
tree579ca25730412d890210721ac09b9ba4c02b9ca2
parent174192d5605c51b41f39bb8c5de1abc1edc8a106 (diff)
kms capture: implement hdr to sdr tonemapping
-rw-r--r--.gitignore3
-rw-r--r--README.md7
-rw-r--r--TODO2
-rw-r--r--include/color_conversion.h16
-rw-r--r--include/defs.h6
-rw-r--r--include/egl.h1
-rw-r--r--include/window/window.h2
-rw-r--r--protocol/kde-output-device-v2.xml633
-rw-r--r--protocol/meson.build1
-rw-r--r--src/capture/kms.c178
-rw-r--r--src/color_conversion.c342
-rw-r--r--src/window/wayland.c270
-rw-r--r--src/window/window.c6
13 files changed, 1431 insertions, 36 deletions
diff --git a/.gitignore b/.gitignore
index 5172b61..e5b10b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,9 @@ tests/compile_commands.json
**/xdg-output-unstable-v1-client-protocol.h
**/xdg-output-unstable-v1-protocol.c
+**/kde-output-device-v2-client-protocol.h
+**/kde-output-device-v2-protocol.c
+
.clangd/
.cache/
.vscode/
diff --git a/README.md b/README.md
index a39f011..c63603a 100644
--- a/README.md
+++ b/README.md
@@ -236,15 +236,10 @@ Alternatively you can record with h264 codec (-k h264, which is also the default
This can happen if your video player is missing the H264/HEVC video codecs. Either install the codecs or use mpv.
## 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`).
-## The colors look washed out when recording a monitor with HDR enabled
-You have to either record in hdr mode (-k `hevc_hdr` or -k `av1_hdr` option) to record a HDR video or record with desktop portal option (`-w portal`) to turn the HDR recording into SDR.
-## GPU Screen Recorder records night light when recording in HDR mode
+## 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.
## 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).
-## Colors look incorrect when recording HDR (with hevc_hdr/av1_hdr) or using an ICC profile
-KDE Plasma version 6.2 broke HDR and ICC profiles for screen recorders. This was changed in KDE plasma version 6.3 and recording HDR works now, as long as you set HDR brightness to 100% (which means setting "Maximum SDR Brightness" in KDE plasma display settings to 203) and set color accuracy to "Prefer color accuracy". If you want to convert HDR to SDR then record with desktop portal option (`-w portal`) instead.
-I don't know how well recording HDR works in wayland compositors other than KDE plasma.
## GPU Screen Recorder starts lagging after 30-40 minutes when launching GPU Screen Recorder from steam command launcher
This is a [steam issue](https://github.com/ValveSoftware/steam-for-linux/issues/11446). Prepend the gpu-screen-recorder command with `LD_PREFIX=""`, for example `LD_PREFIX="" gpu-screen-recorder -w screen -o video.mp4`.
## How do I apply audio effects, such as noise suppression?
diff --git a/TODO b/TODO
index 39e1b4b..547114c 100644
--- a/TODO
+++ b/TODO
@@ -64,8 +64,6 @@ Drop frames if live streaming cant keep up with target fps, or dynamically chang
Instead of sending a big list of drm data back to kms client, send the monitor we want to record to kms server and the server should respond with only the matching monitor, and cursor.
-Tonemap hdr to sdr when hdr is enabled and when hevc_hdr/av1_hdr is not used.
-
Rotate cursor texture properly (around top left origin).
Modify ffmpeg to accept opengl texture for nvenc encoding. Removes extra buffers and copies.
diff --git a/include/color_conversion.h b/include/color_conversion.h
index f1f1b29..7950472 100644
--- a/include/color_conversion.h
+++ b/include/color_conversion.h
@@ -6,7 +6,7 @@
#include "vec2.h"
#include <stdbool.h>
-#define GSR_COLOR_CONVERSION_MAX_GRAPHICS_SHADERS 12
+#define GSR_COLOR_CONVERSION_MAX_GRAPHICS_SHADERS 18
#define GSR_COLOR_CONVERSION_MAX_FRAMEBUFFERS 2
typedef enum {
@@ -37,6 +37,10 @@ typedef enum {
typedef struct {
int rotation_matrix;
int offset;
+ int hdr_source_max_pq;
+ int hdr_sdr_white_pq;
+ int hdr_sdr_white_scale;
+ int gamma_lut_enabled;
} gsr_color_graphics_uniforms;
typedef struct {
@@ -64,6 +68,13 @@ typedef struct {
unsigned int vertex_buffer_object_id;
bool schedule_clear;
+ float hdr_source_max_pq;
+ float hdr_sdr_white_pq;
+ float hdr_sdr_white_scale;
+ unsigned int gamma_lut_texture_id;
+ bool gamma_lut_apply;
+ bool hdr_shaders_loaded;
+ bool hdr_shaders_load_failed;
} gsr_color_conversion;
int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params);
@@ -71,6 +82,9 @@ void gsr_color_conversion_deinit(gsr_color_conversion *self);
void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture);
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);
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/defs.h b/include/defs.h
index d6de6d8..7aa9d32 100644
--- a/include/defs.h
+++ b/include/defs.h
@@ -26,6 +26,12 @@ typedef enum {
GSR_MONITOR_ROT_270,
} gsr_monitor_rotation;
+typedef struct {
+ bool hdr_enabled;
+ float sdr_white_luminance; /* cd/m² (nits) */
+ float max_peak_luminance; /* cd/m² (nits) */
+} gsr_monitor_hdr_info;
+
typedef enum {
GSR_CONNECTION_X11,
GSR_CONNECTION_WAYLAND,
diff --git a/include/egl.h b/include/egl.h
index 57fa7ff..c9c54e5 100644
--- a/include/egl.h
+++ b/include/egl.h
@@ -112,6 +112,7 @@ typedef void(*__GLXextFuncPtr)(void);
#define GL_RG16 0x822C
#define GL_RGB16 0x8054
#define GL_RGBA32F 0x8814
+#define GL_RGB32F 0x8815
#define GL_UNSIGNED_BYTE 0x1401
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_TEXTURE_WRAP_S 0x2802
diff --git a/include/window/window.h b/include/window/window.h
index 7839f6a..959d325 100644
--- a/include/window/window.h
+++ b/include/window/window.h
@@ -21,6 +21,7 @@ struct gsr_window {
void* (*get_display)(gsr_window *self);
void* (*get_window)(gsr_window *self);
void (*for_each_active_monitor_output_cached)(const gsr_window *self, active_monitor_callback callback, void *userdata);
+ bool (*get_monitor_hdr_info)(const gsr_window *self, const char *monitor_name, gsr_monitor_hdr_info *hdr_info); /* can be NULL */
void *priv;
};
@@ -33,5 +34,6 @@ gsr_display_server gsr_window_get_display_server(const gsr_window *self);
void* gsr_window_get_display(gsr_window *self);
void* gsr_window_get_window(gsr_window *self);
void gsr_window_for_each_active_monitor_output_cached(const gsr_window *self, active_monitor_callback callback, void *userdata);
+bool gsr_window_get_monitor_hdr_info(const gsr_window *self, const char *monitor_name, gsr_monitor_hdr_info *hdr_info);
#endif /* GSR_WINDOW_H */
diff --git a/protocol/kde-output-device-v2.xml b/protocol/kde-output-device-v2.xml
new file mode 100644
index 0000000..405f038
--- /dev/null
+++ b/protocol/kde-output-device-v2.xml
@@ -0,0 +1,633 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="kde_output_device_v2">
+ <copyright><![CDATA[
+ SPDX-FileCopyrightText: 2008-2011 Kristian Høgsberg
+ SPDX-FileCopyrightText: 2010-2011 Intel Corporation
+ SPDX-FileCopyrightText: 2012-2013 Collabora, Ltd.
+ SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org>
+ SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com>
+
+ SPDX-License-Identifier: MIT-CMU
+ ]]></copyright>
+
+ <interface name="kde_output_device_registry_v2" version="23">
+ <description summary="output devices">
+ This interface can be used to list output devices.
+
+ If this global is bound with a version less than 21, the unsupported_version
+ protocol error will be posted.
+ </description>
+
+ <enum name="error">
+ <description summary="kde_output_device_registry_v2 error values">
+ These errors can be emitted in response to some requests.
+ </description>
+ <entry name="unsupported_version" value="0"
+ summary="the registry was bound with an unsupported version"/>
+ </enum>
+
+ <event name="finished" type="destructor" since="21">
+ <description summary="no new output announcements">
+ This event is sent in response to the stop request. The compositor will
+ immediately destroy the object after sending this event.
+ </description>
+ </event>
+
+ <request name="stop" since="21">
+ <description summary="stop receiving updates">
+ This request indicates that the client no longer wants to receive new
+ output announcements. The compositor will send the
+ kde_output_device_registry_v2.finished event in response to this request.
+ The compositor may still send new output announcements after calling this
+ request until the kde_output_device_registry_v2.finished event is sent.
+ </description>
+ </request>
+
+ <event name="output" since="21">
+ <description summary="new available output">
+ This event is sent when a new output is connected or after binding this
+ global to list all available outputs.
+ </description>
+ <arg name="output" type="new_id" interface="kde_output_device_v2"/>
+ </event>
+ </interface>
+
+ <interface name="kde_output_device_v2" version="23">
+ <description summary="output configuration representation">
+ An output device describes a display device available to the compositor.
+ output_device is similar to wl_output, but focuses on output
+ configuration management.
+
+ A client can query all global output_device objects to enlist all
+ available display devices, even those that may currently not be
+ represented by the compositor as a wl_output.
+
+ The client sends configuration changes to the server through the
+ outputconfiguration interface, and the server applies the configuration
+ changes to the hardware and signals changes to the output devices
+ accordingly.
+
+ This object is published as global during start up for every available
+ display devices, or when one later becomes available, for example by
+ being hotplugged via a physical connector.
+
+ Warning! The protocol described in this file is a desktop environment
+ implementation detail. Regular clients must not use this protocol.
+ Backward incompatible changes may be added without bumping the major
+ version of the extension.
+ </description>
+
+ <enum name="subpixel">
+ <description summary="subpixel geometry information">
+ This enumeration describes how the physical pixels on an output are
+ laid out.
+ </description>
+ <entry name="unknown" value="0"/>
+ <entry name="none" value="1"/>
+ <entry name="horizontal_rgb" value="2"/>
+ <entry name="horizontal_bgr" value="3"/>
+ <entry name="vertical_rgb" value="4"/>
+ <entry name="vertical_bgr" value="5"/>
+ </enum>
+
+ <enum name="transform">
+ <description summary="transform from framebuffer to output">
+ This describes the transform, that a compositor will apply to a
+ surface to compensate for the rotation or mirroring of an
+ output device.
+
+ The flipped values correspond to an initial flip around a
+ vertical axis followed by rotation.
+
+ The purpose is mainly to allow clients to render accordingly and
+ tell the compositor, so that for fullscreen surfaces, the
+ compositor is still able to scan out directly client surfaces.
+ </description>
+
+ <entry name="normal" value="0"/>
+ <entry name="90" value="1"/>
+ <entry name="180" value="2"/>
+ <entry name="270" value="3"/>
+ <entry name="flipped" value="4"/>
+ <entry name="flipped_90" value="5"/>
+ <entry name="flipped_180" value="6"/>
+ <entry name="flipped_270" value="7"/>
+ </enum>
+
+ <event name="geometry">
+ <description summary="geometric properties of the output">
+ The geometry event describes geometric properties of the output.
+ The event is sent when binding to the output object and whenever
+ any of the properties change.
+ </description>
+ <arg name="x" type="int"
+ summary="x position within the global compositor space"/>
+ <arg name="y" type="int"
+ summary="y position within the global compositor space"/>
+ <arg name="physical_width" type="int"
+ summary="width in millimeters of the output"/>
+ <arg name="physical_height" type="int"
+ summary="height in millimeters of the output"/>
+ <arg name="subpixel" type="int"
+ summary="subpixel orientation of the output"/>
+ <arg name="make" type="string"
+ summary="textual description of the manufacturer"/>
+ <arg name="model" type="string"
+ summary="textual description of the model"/>
+ <arg name="transform" type="int"
+ summary="transform that maps framebuffer to output"/>
+ </event>
+
+ <event name="current_mode">
+ <description summary="current mode">
+ This event describes the mode currently in use for this head. It is only
+ sent if the output is enabled.
+ </description>
+ <arg name="mode" type="object" interface="kde_output_device_mode_v2"/>
+ </event>
+
+ <event name="mode">
+ <description summary="advertise available output modes and current one">
+ The mode event describes an available mode for the output.
+
+ When the client binds to the output_device object, the server sends this
+ event once for every available mode the output_device can be operated by.
+
+ There will always be at least one event sent out on initial binding,
+ which represents the current mode.
+
+ Later if an output changes, its mode event is sent again for the
+ eventual added modes and lastly the current mode. In other words, the
+ current mode is always represented by the latest event sent with the current
+ flag set.
+
+ The size of a mode is given in physical hardware units of the output device.
+ This is not necessarily the same as the output size in the global compositor
+ space. For instance, the output may be scaled, as described in
+ kde_output_device_v2.scale, or transformed, as described in
+ kde_output_device_v2.transform.
+ </description>
+ <arg name="mode" type="new_id" interface="kde_output_device_mode_v2"/>
+ </event>
+
+ <event name="done">
+ <description summary="sent all information about output">
+ This event is sent after all other properties have been
+ sent on binding to the output object as well as after any
+ other output property change have been applied later on.
+ This allows to see changes to the output properties as atomic,
+ even if multiple events successively announce them.
+ </description>
+ </event>
+
+ <event name="scale">
+ <description summary="output scaling properties">
+ This event contains scaling geometry information
+ that is not in the geometry event. It may be sent after
+ binding the output object or if the output scale changes
+ later. If it is not sent, the client should assume a
+ scale of 1.
+
+ A scale larger than 1 means that the compositor will
+ automatically scale surface buffers by this amount
+ when rendering. This is used for high resolution
+ displays where applications rendering at the native
+ resolution would be too small to be legible.
+
+ It is intended that scaling aware clients track the
+ current output of a surface, and if it is on a scaled
+ output it should use wl_surface.set_buffer_scale with
+ the scale of the output. That way the compositor can
+ avoid scaling the surface, and the client can supply
+ a higher detail image.
+ </description>
+ <arg name="factor" type="fixed" summary="scaling factor of output"/>
+ </event>
+
+ <event name="edid">
+ <description summary="advertise EDID data for the output">
+ The edid event encapsulates the EDID data for the outputdevice.
+
+ The event is sent when binding to the output object. The EDID
+ data may be empty, in which case this event is sent anyway.
+ If the EDID information is empty, you can fall back to the name
+ et al. properties of the outputdevice.
+ </description>
+ <arg name="raw" type="string" summary="base64-encoded EDID string"/>
+ </event>
+
+ <event name="enabled">
+ <description summary="output is enabled or disabled">
+ The enabled event notifies whether this output is currently
+ enabled and used for displaying content by the server.
+ The event is sent when binding to the output object and
+ whenever later on an output changes its state by becoming
+ enabled or disabled.
+ </description>
+ <arg name="enabled" type="int" summary="output enabled state"/>
+ </event>
+
+ <event name="uuid">
+ <description summary="A unique id for this outputdevice">
+ The uuid can be used to identify the output. It's controlled by
+ the server entirely. The server should make sure the uuid is
+ persistent across restarts. An empty uuid is considered invalid.
+ </description>
+ <arg name="uuid" type="string" summary="output devices ID"/>
+ </event>
+
+ <event name="serial_number">
+ <description summary="Serial Number">
+ Serial ID of the monitor, sent on startup before the first done event.
+ </description>
+ <arg name="serialNumber" type="string"
+ summary="textual representation of serial number"/>
+ </event>
+ <event name="eisa_id">
+ <description summary="EISA ID">
+ EISA ID of the monitor, sent on startup before the first done event.
+ </description>
+ <arg name="eisaId" type="string"
+ summary="textual representation of EISA identifier"/>
+ </event>
+
+ <enum name="capability" bitfield="true">
+ <description summary="describes capabilities of the outputdevice">
+ Describes what capabilities this device has.
+ </description>
+ <entry name="overscan" value="0x1"
+ summary="if this output_device can use overscan"/>
+ <entry name="vrr" value="0x2"
+ summary="if this outputdevice supports variable refresh rate"/>
+ <entry name="rgb_range" value="0x4"
+ summary="if setting the rgb range is possible"/>
+ <entry name="high_dynamic_range" value="0x8" since="3"
+ summary="if this outputdevice supports high dynamic range"/>
+ <entry name="wide_color_gamut" value="0x10" since="3"
+ summary="if this outputdevice supports a wide color gamut"/>
+ <entry name="auto_rotate" value="0x20" since="4"
+ summary="if this outputdevice supports autorotation"/>
+ <entry name="icc_profile" value="0x40" since="5"
+ summary="if this outputdevice supports icc profiles"/>
+ <entry name="brightness" value="0x80" since="9"
+ summary="if this outputdevice supports the brightness setting"/>
+ <entry name="built_in_color" value="0x100" since="12"
+ summary="if this outputdevice supports the built-in color profile"/>
+ <entry name="ddc_ci" value="0x200" since="14"
+ summary="if this outputdevice supports DDC/CI"/>
+ <entry name="max_bits_per_color" value="0x400" since="15"
+ summary="if this outputdevice supports setting max bpc"/>
+ <entry name="edr" value="0x800" since="16"
+ summary="if this outputdevice supports EDR"/>
+ <entry name="sharpness" value="0x1000" since="17"
+ summary="if this outputdevice supports the sharpness setting"/>
+ <entry name="custom_modes" value="0x2000" since="18"
+ summary="if this outputdevice supports custom modes"/>
+ <entry name="auto_brightness" value = "0x4000" since="19"/>
+ <entry name="hdr_icc_profile" value="0x8000" since="22"
+ summary="if this outputdevice supports HDR ICC profiles"/>
+ <entry name="abm_level" value="0x10000" since="23"
+ summary="if this outputdevice supports the abm level setting"/>
+ </enum>
+
+ <event name="capabilities">
+ <description summary="capability flags">
+ What capabilities this device has, sent on startup before the first
+ done event.
+ </description>
+ <arg name="flags" type="uint" enum="capability"/>
+ </event>
+
+ <event name="overscan">
+ <description summary="overscan">
+ Overscan value of the monitor in percent, sent on startup before the
+ first done event.
+ </description>
+ <arg name="overscan" type="uint"
+ summary="amount of overscan of the monitor"/>
+ </event>
+
+ <enum name="vrr_policy">
+ <description summary="describes vrr policy">
+ Describes when the compositor may employ variable refresh rate
+ </description>
+ <entry name="never" value="0"/>
+ <entry name="always" value="1"/>
+ <entry name="automatic" value="2"/>
+ </enum>
+
+ <event name="vrr_policy">
+ <description summary="Variable Refresh Rate Policy">
+ What policy the compositor will employ regarding its use of variable
+ refresh rate.
+ </description>
+ <arg name="vrr_policy" type="uint" enum="vrr_policy"/>
+ </event>
+
+ <enum name="rgb_range">
+ <description summary="describes RGB range policy">
+ Whether full or limited color range should be used
+ </description>
+ <entry name="automatic" value="0"/>
+ <entry name="full" value="1"/>
+ <entry name="limited" value="2"/>
+ </enum>
+
+ <event name="rgb_range">
+ <description summary="RGB range">
+ What rgb range the compositor is using for this output
+ </description>
+ <arg name="rgb_range" type="uint" enum="rgb_range"/>
+ </event>
+
+ <event name="name" since="2">
+ <description summary="Output's name">
+ Name of the output, it's useful to cross-reference to an zxdg_output_v1 and ultimately QScreen
+ </description>
+ <arg name="name" type="string"/>
+ </event>
+
+ <event name="high_dynamic_range" since="3">
+ <description summary="if HDR is enabled">
+ Whether or not high dynamic range is enabled for this output
+ </description>
+ <arg name="hdr_enabled" type="uint" summary="1 if enabled, 0 if disabled"/>
+ </event>
+
+ <event name="sdr_brightness" since="3">
+ <description summary="the brightness of sdr if hdr is enabled">
+ If high dynamic range is used, this value defines the brightness in nits for content
+ that's in standard dynamic range format. Note that while the value is in nits, that
+ doesn't necessarily translate to the same brightness on the screen.
+ </description>
+ <arg name="sdr_brightness" type="uint"/>
+ </event>
+
+ <event name="wide_color_gamut" since="3">
+ <description summary="if WCG is enabled">
+ Whether or not the use of a wide color gamut is enabled for this output
+ </description>
+ <arg name="wcg_enabled" type="uint" summary="1 if enabled, 0 if disabled"/>
+ </event>
+
+ <enum name="auto_rotate_policy">
+ <description summary="describes when auto rotate should be used"/>
+ <entry name="never" value="0"/>
+ <entry name="in_tablet_mode" value="1"/>
+ <entry name="always" value="2"/>
+ </enum>
+
+ <event name="auto_rotate_policy" since="4">
+ <description summary="describes when auto rotate is used"/>
+ <arg name="policy" type="uint" enum="auto_rotate_policy"/>
+ </event>
+
+ <event name="icc_profile_path" since="5">
+ <description summary="describes the path to the ICC profile used in SDR mode"/>
+ <arg name="profile_path" type="string"/>
+ </event>
+
+ <event name="brightness_metadata" since="6">
+ <description summary="metadata about the screen's brightness limits"/>
+ <arg name="max_peak_brightness" type="uint" summary="in nits"/>
+ <arg name="max_frame_average_brightness" type="uint" summary="in nits"/>
+ <arg name="min_brightness" type="uint" summary="in 0.0001 nits"/>
+ </event>
+
+ <event name="brightness_overrides" since="6">
+ <description summary="overrides for the screen's brightness limits"/>
+ <arg name="max_peak_brightness" type="int" summary="-1 for no override, positive values are the brightness in nits"/>
+ <arg name="max_average_brightness" type="int" summary="-1 for no override, positive values are the brightness in nits"/>
+ <arg name="min_brightness" type="int" summary="-1 for no override, positive values are the brightness in 0.0001 nits"/>
+ </event>
+
+ <event name="sdr_gamut_wideness" since="6">
+ <description summary="describes which gamut is assumed for sRGB applications">
+ This can be used to provide the colors users assume sRGB applications should have based on the
+ default experience on many modern sRGB screens.
+ </description>
+ <arg name="gamut_wideness" type="uint" summary="0 means rec.709 primaries, 10000 means native primaries"/>
+ </event>
+
+ <enum name="color_profile_source" since="7">
+ <description summary="which source the compositor should use for the color profile on an output"/>
+ <entry name="sRGB" value="0"/>
+ <entry name="ICC" value="1"/>
+ <entry name="EDID" value="2"/>
+ </enum>
+
+ <event name="color_profile_source" since="7">
+ <description summary="describes which source the compositor uses for the color profile on an output in SDR mode"/>
+ <arg name="source" type="uint" enum="color_profile_source"/>
+ </event>
+
+ <event name="brightness" since="8">
+ <description summary="brightness multiplier">
+ This is the brightness modifier of the output. It doesn't specify
+ any absolute values, but is merely a multiplier on top of other
+ brightness values, like sdr_brightness and brightness_metadata.
+ 0 is the minimum brightness (not completely dark) and 10000 is
+ the maximum brightness.
+ This is currently only supported / meaningful while HDR is active.
+ </description>
+ <arg name="brightness" type="uint" summary="brightness in 0-10000"/>
+ </event>
+
+ <enum name="color_power_tradeoff">
+ <description summary="tradeoff between power and accuracy">
+ The compositor can do a lot of things that trade between
+ performance, power and color accuracy. This setting describes
+ a high level preference from the user about in which direction
+ that tradeoff should be made.
+ </description>
+ <entry name="efficiency" value="0" summary="prefer efficiency and performance"/>
+ <entry name="accuracy" value="1" summary="prefer accuracy"/>
+ </enum>
+
+ <event name="color_power_tradeoff" since="10">
+ <description summary="the preferred color/power tradeoff"/>
+ <arg name="preference" type="uint" enum="color_power_tradeoff"/>
+ </event>
+
+ <event name="dimming" since="11">
+ <description summary="dimming multiplier">
+ This is the dimming multiplier of the output. This is similar to
+ the brightness setting, except it's meant to be a temporary setting
+ only, not persistent and may be implemented differently depending
+ on the display.
+ 0 is the minimum dimming factor (not completely dark) and 10000
+ means the output is not dimmed.
+ </description>
+ <arg name="multiplier" type="uint" summary="multiplier in 0-10000"/>
+ </event>
+
+ <event name="replication_source" since="13">
+ <description summary="source output for mirroring"/>
+ <arg name="source" type="string" summary="uuid of the source output"/>
+ </event>
+
+ <event name="ddc_ci_allowed" since="14">
+ <description summary="if DDC/CI should be used to control brightness etc.">
+ If the ddc_ci capability is present, this determines if settings
+ such as brightness, contrast or others should be set using DDC/CI.
+ </description>
+ <arg name="allowed" type="uint" summary="1 if allowed, 0 if disabled"/>
+ </event>
+
+ <event name="max_bits_per_color" since="15">
+ <description summary="override max bpc">
+ This limits the amount of bits per color that are sent to the display.
+ </description>
+ <arg name="max_bpc" type="uint" summary="0 for the default / automatic"/>
+ </event>
+
+ <event name="max_bits_per_color_range" since="15">
+ <description summary="range of max bits per color value"/>
+ <arg name="min_value" type="uint" summary="the minimum supported by the driver"/>
+ <arg name="max_value" type="uint" summary="the maximum supported by the driver"/>
+ </event>
+
+ <event name="automatic_max_bits_per_color_limit" since="15">
+ <description summary="if and to what value automatic max bpc is limited"/>
+ <arg name="max_bpc_limit" type="uint"
+ summary="which value automatic bpc gets limited to. 0 if not limited"/>
+ </event>
+
+ <enum name="edr_policy" since="16">
+ <description summary="when the compositor may make use of EDR"/>
+ <entry name="never" value="0"/>
+ <entry name="always" value="1"/>
+ </enum>
+
+ <event name="edr_policy" since="16">
+ <description summary="when the compositor may apply EDR">
+ When EDR is enabled, the compositor may increase the backlight beyond
+ the user-specified setting, in order to present HDR content on displays
+ without native HDR support.
+ This will usually result in better visuals, but also increases battery
+ usage.
+ </description>
+ <arg name="policy" type="uint" enum="edr_policy"/>
+ </event>
+
+ <event name="sharpness" since="17">
+ <description summary="sharpness strength">
+ This is the sharpness modifier of the output.
+ 0 is sharpness disabled and 10000 is the maximum sharpness
+ </description>
+ <arg name="sharpness" type="uint" summary="sharpness in 0-10000"/>
+ </event>
+
+ <event name="priority" since="18">
+ <description summary="output priority">
+ Describes the position of the output in the output order list,
+ with lower values being earlier in the list. There's no specific
+ value the list has to start at, this value is only used in sorting
+ outputs.
+
+ Note that the output order protocol is not sufficient for this,
+ as an output may not be in the output order if it's disabled or
+ mirroring another screen.
+ </description>
+ <arg name="priority" type="uint" summary="priority"/>
+ </event>
+
+ <event name="auto_brightness" since="20">
+ <description summary="whether or not automatic brightness is enabled"/>
+ <arg name="enabled" type="uint" summary="1 for enabled, 0 for disabled"/>
+ </event>
+
+ <request name="release" type="destructor" since="21">
+ <description summary="destroy the output device">
+ This notifies the compositor that the client no longer wishes to use
+ the kde_output_device_v2 object.
+ </description>
+ </request>
+
+ <event name="removed" since="21">
+ <description summary="the output has been removed">
+ This event is sent when the output device is disconnected and no new
+ updates will be sent. The client should call the kde_output_device_v2.release
+ request after receiving this event.
+ </description>
+ </event>
+
+ <event name="hdr_icc_profile_path" since="22">
+ <description summary="describes the path to the ICC profile used in HDR mode"/>
+ <arg name="profile_path" type="string"/>
+ </event>
+
+ <event name="hdr_color_profile_source" since="22">
+ <description summary="describes which source the compositor uses for the color profile on an output in HDR mode"/>
+ <arg name="source" type="uint" enum="color_profile_source"/>
+ </event>
+
+ <event name="abm_level" since="23">
+ <description summary="allowed level of adaptive backlight modulation">
+ Adaptive backlight modulation is a feature that reduces the backlight
+ and increases contrast of colors on the screen to improve power usage.
+ </description>
+ <arg name="level" type="uint" summary="0 is off, 4 is the maximum level"/>
+ </event>
+ </interface>
+
+ <interface name="kde_output_device_mode_v2" version="22">
+ <description summary="output mode">
+ This object describes an output mode.
+
+ Some heads don't support output modes, in which case modes won't be
+ advertised.
+
+ Properties sent via this interface are applied atomically via the
+ kde_output_device.done event. No guarantees are made regarding the order
+ in which properties are sent.
+ </description>
+
+ <event name="size">
+ <description summary="mode size">
+ This event describes the mode size. The size is given in physical
+ hardware units of the output device. This is not necessarily the same as
+ the output size in the global compositor space. For instance, the output
+ may be scaled or transformed.
+ </description>
+ <arg name="width" type="int" summary="width of the mode in hardware units"/>
+ <arg name="height" type="int" summary="height of the mode in hardware units"/>
+ </event>
+
+ <event name="refresh">
+ <description summary="mode refresh rate">
+ This event describes the mode's fixed vertical refresh rate. It is only
+ sent if the mode has a fixed refresh rate.
+ </description>
+ <arg name="refresh" type="int" summary="vertical refresh rate in mHz"/>
+ </event>
+
+ <event name="preferred">
+ <description summary="mode is preferred">
+ This event advertises this mode as preferred.
+ </description>
+ </event>
+
+ <event name="removed">
+ <description summary="the mode has been destroyed">
+ The compositor will destroy the object immediately after sending this
+ event, so it will become invalid and the client should release any
+ resources associated with it.
+ </description>
+ </event>
+
+ <enum name="flags">
+ <description summary="mode flags"/>
+ <entry name="custom" value="0x1"/>
+ <entry name="reduced_blanking" value="0x2"/>
+ </enum>
+
+ <event name="flags" since="19">
+ <description summary="mode flags">
+ This event describes the mode's flags.
+ </description>
+ <arg name="flags" type="uint" enum="flags"/>
+ </event>
+ </interface>
+
+</protocol>
diff --git a/protocol/meson.build b/protocol/meson.build
index bbdccba..da3b184 100644
--- a/protocol/meson.build
+++ b/protocol/meson.build
@@ -16,6 +16,7 @@ wayland_scanner_client = generator(
protocols = [
'xdg-output-unstable-v1.xml',
+ 'kde-output-device-v2.xml',
]
protocol_src = []
diff --git a/src/capture/kms.c b/src/capture/kms.c
index e061492..039609b 100644
--- a/src/capture/kms.c
+++ b/src/capture/kms.c
@@ -11,6 +11,7 @@
#include <fcntl.h>
#include <xf86drm.h>
+#include <xf86drmMode.h>
#include <drm_fourcc.h>
#include <libavutil/mastering_display_metadata.h>
@@ -19,6 +20,7 @@
#define HDMI_STATIC_METADATA_TYPE1 0
#define HDMI_EOTF_SMPTE_ST2084 2
+#define HDR_PEAK_LUMINANCE_FALLBACK 1000.0f
#define MAX_CONNECTOR_IDS 32
@@ -46,6 +48,13 @@ typedef struct {
struct hdr_output_metadata hdr_metadata;
bool hdr_metadata_set;
+ bool tone_mapping_message_shown;
+
+ int drm_card_fd;
+ uint32_t gamma_lut_connector_id;
+ uint32_t gamma_lut_crtc_id;
+ uint32_t gamma_lut_property_id;
+ uint64_t gamma_lut_blob_id;
bool is_x11;
@@ -80,6 +89,11 @@ static void gsr_capture_kms_stop(gsr_capture_kms *self) {
self->cursor_texture_id = 0;
}
+ if(self->drm_card_fd > 0) {
+ close(self->drm_card_fd);
+ self->drm_card_fd = -1;
+ }
+
// if(self->drm_fd > 0) {
// close(self->drm_fd);
// self->drm_fd = -1;
@@ -151,6 +165,7 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture
gsr_capture_kms *self = cap->priv;
gsr_capture_kms_create_input_texture_ids(self);
+ self->drm_card_fd = open(self->params.egl->card_path, O_RDONLY);
gsr_monitor monitor;
self->monitor_id.num_connector_ids = 0;
@@ -260,6 +275,155 @@ static bool hdr_metadata_is_supported_format(const struct hdr_output_metadata *h
hdr_metadata->hdmi_metadata_type1.eotf == HDMI_EOTF_SMPTE_ST2084;
}
+static float hdr_metadata_get_max_luminance(const struct hdr_output_metadata *hdr_metadata) {
+ float max_luminance = hdr_metadata->hdmi_metadata_type1.max_cll;
+ if(max_luminance <= 0.0f)
+ max_luminance = hdr_metadata->hdmi_metadata_type1.max_display_mastering_luminance;
+ return max_luminance;
+}
+
+static bool drm_plane_is_hdr(const gsr_kms_response_item *drm_fd) {
+ return drm_fd->has_hdr_metadata && hdr_metadata_is_supported_format(&drm_fd->hdr_metadata);
+}
+
+static void gsr_capture_kms_resolve_gamma_lut_property(gsr_capture_kms *self, uint32_t connector_id) {
+ self->gamma_lut_connector_id = connector_id;
+ self->gamma_lut_crtc_id = 0;
+ self->gamma_lut_property_id = 0;
+
+ drmModeConnector *connector = drmModeGetConnectorCurrent(self->drm_card_fd, connector_id);
+ if(!connector)
+ return;
+
+ if(connector->encoder_id) {
+ drmModeEncoder *encoder = drmModeGetEncoder(self->drm_card_fd, connector->encoder_id);
+ if(encoder) {
+ self->gamma_lut_crtc_id = encoder->crtc_id;
+ drmModeFreeEncoder(encoder);
+ }
+ }
+ drmModeFreeConnector(connector);
+
+ if(self->gamma_lut_crtc_id == 0)
+ return;
+
+ drmModeObjectProperties *properties = drmModeObjectGetProperties(self->drm_card_fd, self->gamma_lut_crtc_id, DRM_MODE_OBJECT_CRTC);
+ if(!properties)
+ return;
+
+ for(uint32_t i = 0; i < properties->count_props; ++i) {
+ drmModePropertyRes *property = drmModeGetProperty(self->drm_card_fd, properties->props[i]);
+ if(!property)
+ continue;
+
+ if(strcmp(property->name, "GAMMA_LUT") == 0)
+ self->gamma_lut_property_id = property->prop_id;
+
+ drmModeFreeProperty(property);
+ if(self->gamma_lut_property_id)
+ break;
+ }
+ drmModeFreeObjectProperties(properties);
+}
+
+static uint64_t gsr_capture_kms_get_gamma_lut_blob_id(gsr_capture_kms *self) {
+ if(self->gamma_lut_crtc_id == 0 || self->gamma_lut_property_id == 0)
+ return 0;
+
+ uint64_t blob_id = 0;
+ drmModeObjectProperties *properties = drmModeObjectGetProperties(self->drm_card_fd, self->gamma_lut_crtc_id, DRM_MODE_OBJECT_CRTC);
+ if(!properties)
+ return 0;
+
+ for(uint32_t i = 0; i < properties->count_props; ++i) {
+ if(properties->props[i] == self->gamma_lut_property_id) {
+ blob_id = properties->prop_values[i];
+ break;
+ }
+ }
+ drmModeFreeObjectProperties(properties);
+ return blob_id;
+}
+
+static void gsr_capture_kms_update_gamma_lut(gsr_capture_kms *self, gsr_color_conversion *color_conversion) {
+ if(self->drm_card_fd <= 0)
+ return;
+
+ if(self->drm_fd->connector_id != self->gamma_lut_connector_id)
+ gsr_capture_kms_resolve_gamma_lut_property(self, self->drm_fd->connector_id);
+
+ const uint64_t blob_id = gsr_capture_kms_get_gamma_lut_blob_id(self);
+ if(blob_id == self->gamma_lut_blob_id)
+ return;
+
+ self->gamma_lut_blob_id = blob_id;
+ if(blob_id == 0) {
+ gsr_color_conversion_set_gamma_lut(color_conversion, NULL, 0);
+ return;
+ }
+
+ drmModePropertyBlobRes *blob = drmModeGetPropertyBlob(self->drm_card_fd, blob_id);
+ if(!blob) {
+ self->gamma_lut_blob_id = 0;
+ gsr_color_conversion_set_gamma_lut(color_conversion, NULL, 0);
+ return;
+ }
+
+ const int num_entries = blob->length / sizeof(struct drm_color_lut);
+ const struct drm_color_lut *lut = blob->data;
+ float *rgb_values = malloc(num_entries * 3 * sizeof(float));
+ if(rgb_values && num_entries > 0) {
+ for(int i = 0; i < num_entries; ++i) {
+ rgb_values[i*3 + 0] = (float)lut[i].red / 65535.0f;
+ rgb_values[i*3 + 1] = (float)lut[i].green / 65535.0f;
+ rgb_values[i*3 + 2] = (float)lut[i].blue / 65535.0f;
+ }
+ gsr_color_conversion_set_gamma_lut(color_conversion, rgb_values, num_entries);
+ } else {
+ self->gamma_lut_blob_id = 0;
+ gsr_color_conversion_set_gamma_lut(color_conversion, NULL, 0);
+ }
+
+ free(rgb_values);
+ drmModeFreePropertyBlob(blob);
+}
+
+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);
+
+ const bool tone_map_hdr_to_sdr = !self->params.hdr && plane_is_hdr;
+ if(!tone_map_hdr_to_sdr) {
+ gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, false, 0.0f, 0.0f);
+ return;
+ }
+
+ float sdr_white_luminance = 0.0f;
+ float hdr_peak_luminance = hdr_metadata_get_max_luminance(&drm_fd->hdr_metadata);
+
+ gsr_monitor_hdr_info monitor_hdr_info;
+ if(gsr_window_get_monitor_hdr_info(self->params.egl->window, self->params.display_to_capture, &monitor_hdr_info)) {
+ if(monitor_hdr_info.sdr_white_luminance > 0.0f)
+ sdr_white_luminance = monitor_hdr_info.sdr_white_luminance;
+ if(monitor_hdr_info.max_peak_luminance > 0.0f)
+ hdr_peak_luminance = monitor_hdr_info.max_peak_luminance;
+ }
+
+ if(hdr_peak_luminance <= 0.0f)
+ hdr_peak_luminance = HDR_PEAK_LUMINANCE_FALLBACK;
+
+ if(sdr_white_luminance > hdr_peak_luminance)
+ sdr_white_luminance = hdr_peak_luminance;
+
+ gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, true, hdr_peak_luminance, sdr_white_luminance);
+
+ if(!self->tone_mapping_message_shown) {
+ self->tone_mapping_message_shown = true;
+ fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_to_sdr_tone_mapping: the monitor is in hdr mode, tone mapping the hdr content to sdr (sdr white luminance: %d nits, hdr peak luminance: %d nits). Record with -k hevc_hdr or -k av1_hdr video codec option to record hdr instead\n",
+ (int)(sdr_white_luminance <= 0.0f ? 203.0f : sdr_white_luminance), (int)hdr_peak_luminance);
+ }
+}
+
// TODO: Check if this hdr data can be changed after the call to av_packet_side_data_add
static void gsr_kms_set_hdr_metadata(gsr_capture_kms *self, const gsr_kms_response_item *drm_fd) {
if(self->hdr_metadata_set)
@@ -467,6 +631,8 @@ static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color
if(cursor_image)
self->params.egl->eglDestroyImage(self->params.egl->egl_display, cursor_image);
+ gsr_capture_kms_update_hdr_to_sdr_tone_mapping(self, color_conversion, cursor_drm_fd);
+
self->params.egl->glEnable(GL_SCISSOR_TEST);
self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
@@ -521,6 +687,8 @@ static void gsr_capture_kms_update_connector_ids(gsr_capture_kms *self) {
if(self->is_x11)
return;
+ self->gamma_lut_connector_id = 0;
+
self->monitor_id.num_connector_ids = 0;
const gsr_connection_type connection_type = self->is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM;
// MonitorCallbackUserdata monitor_callback_userdata = {
@@ -571,6 +739,9 @@ static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata *
if(!self->drm_fd)
return;
+ if(drm_plane_is_hdr(self->drm_fd))
+ gsr_capture_kms_update_gamma_lut(self, color_conversion);
+
if(self->drm_fd->has_hdr_metadata && self->params.hdr && hdr_metadata_is_supported_format(&self->drm_fd->hdr_metadata))
gsr_kms_set_hdr_metadata(self, self->drm_fd);
@@ -600,6 +771,8 @@ static void render_monitor_plane(gsr_capture_kms *self, gsr_color_conversion *co
self->params.egl->eglDestroyImage(self->params.egl->egl_display, image);
}
+ gsr_capture_kms_update_hdr_to_sdr_tone_mapping(self, color_conversion, self->drm_fd);
+
gsr_color_conversion_draw(color_conversion, self->external_texture_fallback ? self->external_input_texture_id : self->input_texture_id,
self->target_pos, self->output_size,
capture_pos, self->capture_size, (vec2i){ self->drm_fd->width, self->drm_fd->height },
@@ -658,6 +831,8 @@ static void render_drm_plane(gsr_capture_kms *self, gsr_color_conversion *color_
gsr_capture_kms_bind_image_to_input_texture_with_fallback(self, image);
self->params.egl->eglDestroyImage(self->params.egl->egl_display, image);
+ gsr_capture_kms_update_hdr_to_sdr_tone_mapping(self, color_conversion, plane_drm_fd);
+
self->params.egl->glEnable(GL_SCISSOR_TEST);
self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
@@ -725,6 +900,9 @@ 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);
+
//self->params.egl->glFlush();
//self->params.egl->glFinish();
diff --git a/src/color_conversion.c b/src/color_conversion.c
index c00bfb5..126cc48 100644
--- a/src/color_conversion.c
+++ b/src/color_conversion.c
@@ -3,6 +3,7 @@
#include <stdio.h>
#include <string.h>
#include <assert.h>
+#include <math.h>
#define GRAPHICS_SHADER_INDEX_Y 0
#define GRAPHICS_SHADER_INDEX_UV 1
@@ -16,6 +17,12 @@
#define GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL 9
#define GRAPHICS_SHADER_INDEX_YUYV_TO_RGB 10
#define GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL 11
+#define GRAPHICS_SHADER_INDEX_Y_HDR 12
+#define GRAPHICS_SHADER_INDEX_UV_HDR 13
+#define GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL 14
+#define GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL 15
+#define GRAPHICS_SHADER_INDEX_RGB_HDR 16
+#define GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL 17
/* https://en.wikipedia.org/wiki/YCbCr, see study/color_space_transform_matrix.png */
@@ -45,6 +52,76 @@
" 0.060118, 0.429412, -0.038049, 0.000000,\n" \
" 0.062745, 0.500000, 0.500000, 1.000000);\n"
+#define SDR_WHITE_LUMINANCE_DEFAULT 203.0f /* ITU-R BT.2408 */
+
+/* SMPTE ST 2084 (pq) to sdr with the ITU-R BT.2390 EETF */
+/* https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2390-8-2020-PDF-E.pdf */
+#define HDR_TO_SDR_TONE_MAPPING \
+ "uniform float hdr_source_max_pq; \n" \
+ "uniform float hdr_sdr_white_pq; \n" \
+ "uniform float hdr_sdr_white_scale; \n" \
+ "const float PQ_M1 = 0.1593017578125; \n" \
+ "const float PQ_M2 = 78.84375; \n" \
+ "const float PQ_C1 = 0.8359375; \n" \
+ "const float PQ_C2 = 18.8515625; \n" \
+ "const float PQ_C3 = 18.6875; \n" \
+ "const mat3 BT2020toBT709 = mat3(1.660491, -0.124550, -0.018151, \n" \
+ " -0.587641, 1.132900, -0.100579, \n" \
+ " -0.072850, -0.008349, 1.118730); \n" \
+ "float pq_to_luminance(float pq) { \n" \
+ " float e = pow(max(pq, 0.0), 1.0/PQ_M2); \n" \
+ " return pow(max(e - PQ_C1, 0.0) / (PQ_C2 - PQ_C3*e), 1.0/PQ_M1); \n" \
+ "} \n" \
+ "vec3 pq_to_luminance3(vec3 pq) { \n" \
+ " vec3 e = pow(max(pq, vec3(0.0)), vec3(1.0/PQ_M2)); \n" \
+ " return pow(max(e - PQ_C1, vec3(0.0)) / (PQ_C2 - PQ_C3*e), vec3(1.0/PQ_M1)); \n" \
+ "} \n" \
+ "float luminance_to_pq(float luminance) { \n" \
+ " float l = pow(clamp(luminance, 0.0, 1.0), PQ_M1); \n" \
+ " return pow((PQ_C1 + PQ_C2*l) / (1.0 + PQ_C3*l), PQ_M2); \n" \
+ "} \n" \
+ "float bt2390_eetf(float pq) { \n" \
+ " float max_luminance = hdr_sdr_white_pq / hdr_source_max_pq; \n" \
+ " float knee_start = 1.5*max_luminance - 0.5; \n" \
+ " float e1 = min(pq / hdr_source_max_pq, 1.0); \n" \
+ " if(knee_start < 1.0 && e1 > knee_start) { \n" \
+ " float t = (e1 - knee_start) / (1.0 - knee_start); \n" \
+ " float t2 = t*t; \n" \
+ " float t3 = t2*t; \n" \
+ " e1 = (2.0*t3 - 3.0*t2 + 1.0)*knee_start + (t3 - 2.0*t2 + t)*(1.0 - knee_start) + (-2.0*t3 + 3.0*t2)*max_luminance; \n" \
+ " } \n" \
+ " return e1 * hdr_source_max_pq; \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" \
+ " if(max_channel > 0.0) \n" \
+ " luminance *= pq_to_luminance(bt2390_eetf(luminance_to_pq(max_channel))) / max_channel; \n" \
+ " luminance = BT2020toBT709 * luminance; \n" \
+ " return pow(clamp(luminance * hdr_sdr_white_scale, 0.0, 1.0), vec3(1.0/2.2)); \n" \
+ "} \n"
+
+#define GAMMA_LUT_GLSL \
+ "uniform sampler2D gamma_lut; \n" \
+ "uniform float gamma_lut_enabled; \n" \
+ "vec3 apply_gamma_lut(vec3 color) { \n" \
+ " int last_index = textureSize(gamma_lut, 0).x - 1; \n" \
+ " vec3 pos = clamp(color, 0.0, 1.0) * float(last_index); \n" \
+ " ivec3 index0 = ivec3(pos); \n" \
+ " ivec3 index1 = min(index0 + 1, ivec3(last_index)); \n" \
+ " vec3 t = pos - vec3(index0); \n" \
+ " float r = mix(texelFetch(gamma_lut, ivec2(index0.r, 0), 0).r, texelFetch(gamma_lut, ivec2(index1.r, 0), 0).r, t.r); \n" \
+ " float g = mix(texelFetch(gamma_lut, ivec2(index0.g, 0), 0).g, texelFetch(gamma_lut, ivec2(index1.g, 0), 0).g, t.g); \n" \
+ " float b = mix(texelFetch(gamma_lut, ivec2(index0.b, 0), 0).b, texelFetch(gamma_lut, ivec2(index1.b, 0), 0).b, t.b); \n" \
+ " return vec3(r, g, b); \n" \
+ "} \n"
+
+#define APPLY_HDR_TO_SDR_TONE_MAPPING \
+ " if(gamma_lut_enabled > 0.5) \n" \
+ " pixel.rgb = apply_gamma_lut(pixel.rgb); \n" \
+ " if(hdr_source_max_pq > 0.0) \n" \
+ " pixel.rgb = tone_map_hdr_to_sdr(pixel.rgb); \n"
+
static const char* color_format_range_get_transform_matrix(gsr_destination_color color_format, gsr_color_range color_range) {
switch(color_format) {
case GSR_DESTINATION_COLOR_NV12: {
@@ -73,7 +150,7 @@ static const char* color_format_range_get_transform_matrix(gsr_destination_color
return NULL;
}
-static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture, bool hdr) {
const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
char vertex_shader[2048];
@@ -92,13 +169,14 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
"} \n");
const char *brightness = color_format == GSR_DESTINATION_COLOR_NV12 ? "1.007049345" : "1.0";
- char main_code[512];
+ char main_code[1024];
snprintf(main_code, sizeof(main_code),
" vec4 pixel = texture(tex1, texcoords_out); \n"
+ "%s"
" FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x*%s; \n"
- " FragColor.w = pixel.a; \n", brightness);
+ " FragColor.w = pixel.a; \n", hdr ? APPLY_HDR_TO_SDR_TONE_MAPPING : "", brightness);
- char fragment_shader[2048];
+ char fragment_shader[16384];
if(external_texture) {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -109,10 +187,11 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
"uniform samplerExternalOES tex1; \n"
"out vec4 FragColor; \n"
"%s"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -121,10 +200,11 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
"uniform sampler2D tex1; \n"
"out vec4 FragColor; \n"
"%s"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -134,10 +214,18 @@ static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_gr
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = egl->glGetUniformLocation(shader->program_id, "hdr_source_max_pq");
+ 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");
+
+ egl->glUseProgram(shader->program_id);
+ egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
+ egl->glUseProgram(0);
return 0;
}
-static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture, bool hdr) {
const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
char vertex_shader[2048];
@@ -155,12 +243,14 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
" gl_Position = (vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0)) * vec4(0.5, 0.5, 1.0, 1.0) - vec4(0.5, 0.5, 0.0, 0.0); \n"
"} \n");
- const char *main_code =
+ char main_code[1024];
+ snprintf(main_code, sizeof(main_code),
" vec4 pixel = texture(tex1, texcoords_out); \n"
+ "%s"
" FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n"
- " FragColor.w = pixel.a; \n";
+ " FragColor.w = pixel.a; \n", hdr ? APPLY_HDR_TO_SDR_TONE_MAPPING : "");
- char fragment_shader[2048];
+ char fragment_shader[16384];
if(external_texture) {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -171,10 +261,11 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
"uniform samplerExternalOES tex1; \n"
"out vec4 FragColor; \n"
"%s"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -183,10 +274,11 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
"uniform sampler2D tex1; \n"
"out vec4 FragColor; \n"
"%s"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", color_transform_matrix, main_code);
+ "} \n", color_transform_matrix, hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -196,10 +288,18 @@ static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gs
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = egl->glGetUniformLocation(shader->program_id, "hdr_source_max_pq");
+ 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");
+
+ egl->glUseProgram(shader->program_id);
+ egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
+ egl->glUseProgram(0);
return 0;
}
-static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture) {
+static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture, bool hdr) {
char vertex_shader[2048];
snprintf(vertex_shader, sizeof(vertex_shader),
"#version 300 es \n"
@@ -215,11 +315,13 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
" gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n"
"} \n");
- const char *main_code =
+ char main_code[1024];
+ snprintf(main_code, sizeof(main_code),
" vec4 pixel = texture(tex1, texcoords_out); \n"
- " FragColor = pixel; \n";
+ "%s"
+ " FragColor = pixel; \n", hdr ? APPLY_HDR_TO_SDR_TONE_MAPPING : "");
- char fragment_shader[2048];
+ char fragment_shader[16384];
if(external_texture) {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -229,10 +331,11 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
"in vec2 texcoords_out; \n"
"uniform samplerExternalOES tex1; \n"
"out vec4 FragColor; \n"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", main_code);
+ "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
} else {
snprintf(fragment_shader, sizeof(fragment_shader),
"#version 300 es \n"
@@ -240,10 +343,11 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
"in vec2 texcoords_out; \n"
"uniform sampler2D tex1; \n"
"out vec4 FragColor; \n"
+ "%s"
"void main() \n"
"{ \n"
"%s"
- "} \n", main_code);
+ "} \n", hdr ? HDR_TO_SDR_TONE_MAPPING GAMMA_LUT_GLSL : "", main_code);
}
if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
@@ -253,6 +357,14 @@ static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, g
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = egl->glGetUniformLocation(shader->program_id, "hdr_source_max_pq");
+ 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");
+
+ egl->glUseProgram(shader->program_id);
+ egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1);
+ egl->glUseProgram(0);
return 0;
}
@@ -311,6 +423,10 @@ static int load_graphics_shader_yuyv_to_y(gsr_shader *shader, gsr_egl *egl, gsr_
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = -1;
+ uniforms->hdr_sdr_white_pq = -1;
+ uniforms->hdr_sdr_white_scale = -1;
+ uniforms->gamma_lut_enabled = -1;
return 0;
}
@@ -382,6 +498,10 @@ static unsigned int load_graphics_shader_yuyv_to_uv(gsr_shader *shader, gsr_egl
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = -1;
+ uniforms->hdr_sdr_white_pq = -1;
+ uniforms->hdr_sdr_white_scale = -1;
+ uniforms->gamma_lut_enabled = -1;
return 0;
}
@@ -459,9 +579,75 @@ static unsigned int load_graphics_shader_yuyv_to_rgb(gsr_shader *shader, gsr_egl
gsr_shader_bind_attribute_location(shader, "texcoords", 1);
uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix");
+ uniforms->hdr_source_max_pq = -1;
+ uniforms->hdr_sdr_white_pq = -1;
+ uniforms->hdr_sdr_white_scale = -1;
+ uniforms->gamma_lut_enabled = -1;
return 0;
}
+static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion *self) {
+ switch(self->params.destination_color) {
+ case GSR_DESTINATION_COLOR_NV12:
+ case GSR_DESTINATION_COLOR_P010: {
+ if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr)\n");
+ return false;
+ }
+
+ if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr)\n");
+ return false;
+ }
+
+ if(self->params.load_external_image_shader) {
+ if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr, external)\n");
+ return false;
+ }
+
+ if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr, external)\n");
+ return false;
+ }
+ }
+ break;
+ }
+ case GSR_DESTINATION_COLOR_RGB: {
+ if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR], false, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr)\n");
+ return false;
+ }
+
+ if(self->params.load_external_image_shader) {
+ if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], true, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr, external)\n");
+ return false;
+ }
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+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);
+ if(!hdr_enabled || self->hdr_shaders_load_failed)
+ return false;
+
+ if(self->hdr_shaders_loaded)
+ return true;
+
+ if(!gsr_color_conversion_load_hdr_graphics_shaders(self)) {
+ self->hdr_shaders_load_failed = true;
+ return false;
+ }
+
+ self->hdr_shaders_loaded = true;
+ return true;
+}
+
static int load_framebuffers(gsr_color_conversion *self) {
/* TODO: Only generate the necessary amount of framebuffers (self->params.num_destination_textures) */
const unsigned int draw_buffer = GL_COLOR_ATTACHMENT0;
@@ -515,12 +701,12 @@ static bool gsr_color_conversion_load_graphics_shaders(gsr_color_conversion *sel
switch(self->params.destination_color) {
case GSR_DESTINATION_COLOR_NV12:
case GSR_DESTINATION_COLOR_P010: {
- if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y], self->params.destination_color, self->params.color_range, false) != 0) {
+ if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y], self->params.destination_color, self->params.color_range, false, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader\n");
return false;
}
- if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV], self->params.destination_color, self->params.color_range, false) != 0) {
+ if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV], self->params.destination_color, self->params.color_range, false, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader\n");
return false;
}
@@ -537,7 +723,7 @@ static bool gsr_color_conversion_load_graphics_shaders(gsr_color_conversion *sel
break;
}
case GSR_DESTINATION_COLOR_RGB: {
- if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB], false) != 0) {
+ if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB], false, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader\n");
return false;
}
@@ -556,12 +742,12 @@ static bool gsr_color_conversion_load_external_graphics_shaders(gsr_color_conver
switch(self->params.destination_color) {
case GSR_DESTINATION_COLOR_NV12:
case GSR_DESTINATION_COLOR_P010: {
- if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.destination_color, self->params.color_range, true) != 0) {
+ if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader (external)\n");
return false;
}
- if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.destination_color, self->params.color_range, true) != 0) {
+ if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader (external)\n");
return false;
}
@@ -578,7 +764,7 @@ static bool gsr_color_conversion_load_external_graphics_shaders(gsr_color_conver
break;
}
case GSR_DESTINATION_COLOR_RGB: {
- if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], true) != 0) {
+ if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], true, false) != 0) {
fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader (external)\n");
return false;
}
@@ -643,6 +829,11 @@ void gsr_color_conversion_deinit(gsr_color_conversion *self) {
if(!self->params.egl)
return;
+ if(self->gamma_lut_texture_id) {
+ self->params.egl->glDeleteTextures(1, &self->gamma_lut_texture_id);
+ self->gamma_lut_texture_id = 0;
+ }
+
if(self->vertex_buffer_object_id) {
self->params.egl->glDeleteBuffers(1, &self->vertex_buffer_object_id);
self->vertex_buffer_object_id = 0;
@@ -732,6 +923,14 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glBindTexture(texture_target, texture_id);
gsr_color_conversion_swizzle_texture_source(self, texture_target, source_color);
+ const bool use_hdr_shaders = gsr_color_conversion_load_hdr_graphics_shaders_if_needed(self);
+ const float gamma_lut_enabled = (use_hdr_shaders && self->gamma_lut_apply && self->gamma_lut_texture_id) ? 1.0f : 0.0f;
+ if(gamma_lut_enabled != 0.0f) {
+ self->params.egl->glActiveTexture(GL_TEXTURE1);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->gamma_lut_texture_id);
+ self->params.egl->glActiveTexture(GL_TEXTURE0);
+ }
+
const vec2f pos_norm = {
((float)destination_pos.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f,
((float)destination_pos.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f,
@@ -792,20 +991,39 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
//cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example)
- int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_Y_EXTERNAL : GRAPHICS_SHADER_INDEX_Y;
+ int shader_index;
+ if(use_hdr_shaders)
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL : GRAPHICS_SHADER_INDEX_Y_HDR;
+ else
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_Y_EXTERNAL : GRAPHICS_SHADER_INDEX_Y;
gsr_shader_use(&self->graphics_shaders[shader_index]);
self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix);
self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ if(use_hdr_shaders) {
+ self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_source_max_pq, self->hdr_source_max_pq);
+ 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->glDrawArrays(GL_TRIANGLES, 0, 6);
if(self->params.num_destination_textures > 1) {
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
//cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT);
- shader_index = external_texture ? GRAPHICS_SHADER_INDEX_UV_EXTERNAL : GRAPHICS_SHADER_INDEX_UV;
+ if(use_hdr_shaders)
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL : GRAPHICS_SHADER_INDEX_UV_HDR;
+ else
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_UV_EXTERNAL : GRAPHICS_SHADER_INDEX_UV;
gsr_shader_use(&self->graphics_shaders[shader_index]);
self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix);
self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ if(use_hdr_shaders) {
+ self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_source_max_pq, self->hdr_source_max_pq);
+ 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->glDrawArrays(GL_TRIANGLES, 0, 6);
}
break;
@@ -814,10 +1032,20 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
//cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example)
- const int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_RGB_EXTERNAL : GRAPHICS_SHADER_INDEX_RGB;
+ int shader_index;
+ if(use_hdr_shaders)
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL : GRAPHICS_SHADER_INDEX_RGB_HDR;
+ else
+ shader_index = external_texture ? GRAPHICS_SHADER_INDEX_RGB_EXTERNAL : GRAPHICS_SHADER_INDEX_RGB;
gsr_shader_use(&self->graphics_shaders[shader_index]);
self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix);
self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ if(use_hdr_shaders) {
+ self->params.egl->glUniform1f(self->graphics_uniforms[shader_index].hdr_source_max_pq, self->hdr_source_max_pq);
+ 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->glDrawArrays(GL_TRIANGLES, 0, 6);
break;
}
@@ -868,6 +1096,11 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glBindVertexArray(0);
self->params.egl->glUseProgram(0);
gsr_color_conversion_swizzle_reset(self, texture_target, source_color);
+ if(gamma_lut_enabled != 0.0f) {
+ self->params.egl->glActiveTexture(GL_TEXTURE1);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+ self->params.egl->glActiveTexture(GL_TEXTURE0);
+ }
self->params.egl->glBindTexture(texture_target, 0);
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
@@ -937,6 +1170,61 @@ void gsr_color_conversion_clear(gsr_color_conversion *self) {
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
+static float luminance_to_pq(float luminance) {
+ const float l = powf(luminance / 10000.0f, 0.1593017578125f);
+ return powf((0.8359375f + 18.8515625f * l) / (1.0f + 18.6875f * l), 78.84375f);
+}
+
+void gsr_color_conversion_set_hdr_to_sdr_tone_mapping(gsr_color_conversion *self, bool enable, float hdr_peak_luminance, float sdr_white_luminance) {
+ if(!enable) {
+ self->hdr_source_max_pq = 0.0f;
+ return;
+ }
+
+ if(sdr_white_luminance <= 0.0f)
+ sdr_white_luminance = SDR_WHITE_LUMINANCE_DEFAULT;
+
+ if(sdr_white_luminance < 80.0f)
+ sdr_white_luminance = 80.0f;
+ else if(sdr_white_luminance > 10000.0f)
+ sdr_white_luminance = 10000.0f;
+
+ if(hdr_peak_luminance < sdr_white_luminance)
+ hdr_peak_luminance = sdr_white_luminance;
+ else if(hdr_peak_luminance > 10000.0f)
+ hdr_peak_luminance = 10000.0f;
+
+ self->hdr_source_max_pq = luminance_to_pq(hdr_peak_luminance);
+ self->hdr_sdr_white_pq = luminance_to_pq(sdr_white_luminance);
+ self->hdr_sdr_white_scale = 10000.0f / sdr_white_luminance;
+}
+
+void gsr_color_conversion_set_gamma_lut(gsr_color_conversion *self, const float *rgb_values, int num_entries) {
+ if(!rgb_values || num_entries <= 0) {
+ if(self->gamma_lut_texture_id) {
+ self->params.egl->glDeleteTextures(1, &self->gamma_lut_texture_id);
+ self->gamma_lut_texture_id = 0;
+ }
+ return;
+ }
+
+ if(self->gamma_lut_texture_id == 0) {
+ self->params.egl->glGenTextures(1, &self->gamma_lut_texture_id);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->gamma_lut_texture_id);
+ self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ } else {
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->gamma_lut_texture_id);
+ }
+
+ self->params.egl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, num_entries, 1, 0, GL_RGB, GL_FLOAT, rgb_values);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+void gsr_color_conversion_enable_gamma_lut(gsr_color_conversion *self, bool enable) {
+ self->gamma_lut_apply = enable;
+}
+
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/window/wayland.c b/src/window/wayland.c
index f7218b3..d572d13 100644
--- a/src/window/wayland.c
+++ b/src/window/wayland.c
@@ -11,6 +11,7 @@
#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-output-unstable-v1-client-protocol.h"
+#include "kde-output-device-v2-client-protocol.h"
#define GSR_MAX_OUTPUTS 32
@@ -27,6 +28,20 @@ typedef struct {
char *name;
} gsr_wayland_output;
+#define KDE_PLASMA_ASSUMED_MONITOR_PEAK_LUMINANCE 800.0f
+#define KDE_BRIGHTNESS_MULTIPLIER_MAX 10000
+
+typedef struct {
+ struct kde_output_device_v2 *device;
+ char *name;
+ bool hdr_enabled;
+ uint32_t sdr_brightness; /* cd/m² (nits) */
+ uint32_t max_peak_brightness; /* cd/m² (nits) */
+ int32_t max_peak_brightness_override; /* cd/m² (nits), -1 if not set */
+ uint32_t brightness; /* 0-10000 */
+ uint32_t dimming; /* 0-10000 */
+} gsr_kde_output_device;
+
struct gsr_window_wayland {
struct wl_display *display;
struct wl_egl_window *window;
@@ -36,6 +51,8 @@ struct gsr_window_wayland {
gsr_wayland_output outputs[GSR_MAX_OUTPUTS];
int num_outputs;
struct zxdg_output_manager_v1 *xdg_output_manager;
+ struct kde_output_device_registry_v2 *kde_output_device_registry;
+ gsr_kde_output_device kde_output_devices[GSR_MAX_OUTPUTS];
};
static void output_handle_geometry(void *data, struct wl_output *wl_output,
@@ -99,6 +116,208 @@ static const struct wl_output_listener output_listener = {
.description = output_handle_description,
};
+static void kde_output_device_free(gsr_kde_output_device *device) {
+ if(device->device) {
+ kde_output_device_v2_destroy(device->device);
+ device->device = NULL;
+ }
+
+ if(device->name) {
+ free(device->name);
+ device->name = NULL;
+ }
+}
+
+static void kde_output_device_handle_geometry(void *data, struct kde_output_device_v2 *device, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height,
+ int32_t subpixel, const char *make, const char *model, int32_t transform) {
+ (void)data; (void)device; (void)x; (void)y; (void)physical_width; (void)physical_height; (void)subpixel; (void)make; (void)model; (void)transform;
+}
+
+static void kde_output_device_handle_current_mode(void *data, struct kde_output_device_v2 *device, struct kde_output_device_mode_v2 *mode) {
+ (void)data; (void)device; (void)mode;
+}
+
+static void kde_output_device_handle_mode(void *data, struct kde_output_device_v2 *device, struct kde_output_device_mode_v2 *mode) {
+ (void)data; (void)device;
+ kde_output_device_mode_v2_destroy(mode);
+}
+
+static void kde_output_device_handle_done(void *data, struct kde_output_device_v2 *device) {
+ (void)data; (void)device;
+}
+
+static void kde_output_device_handle_scale(void *data, struct kde_output_device_v2 *device, wl_fixed_t factor) {
+ (void)data; (void)device; (void)factor;
+}
+
+static void kde_output_device_handle_edid(void *data, struct kde_output_device_v2 *device, const char *raw) {
+ (void)data; (void)device; (void)raw;
+}
+
+static void kde_output_device_handle_enabled(void *data, struct kde_output_device_v2 *device, int32_t enabled) {
+ (void)data; (void)device; (void)enabled;
+}
+
+static void kde_output_device_handle_string_noop(void *data, struct kde_output_device_v2 *device, const char *value) {
+ (void)data; (void)device; (void)value;
+}
+
+static void kde_output_device_handle_uint_noop(void *data, struct kde_output_device_v2 *device, uint32_t value) {
+ (void)data; (void)device; (void)value;
+}
+
+static void kde_output_device_handle_name(void *data, struct kde_output_device_v2 *device, const char *name) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ if(gsr_device->name) {
+ free(gsr_device->name);
+ gsr_device->name = NULL;
+ }
+ gsr_device->name = strdup(name);
+}
+
+static void kde_output_device_handle_high_dynamic_range(void *data, struct kde_output_device_v2 *device, uint32_t hdr_enabled) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->hdr_enabled = hdr_enabled != 0;
+}
+
+static void kde_output_device_handle_sdr_brightness(void *data, struct kde_output_device_v2 *device, uint32_t sdr_brightness) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->sdr_brightness = sdr_brightness;
+}
+
+static void kde_output_device_handle_brightness_metadata(void *data, struct kde_output_device_v2 *device, uint32_t max_peak_brightness, uint32_t max_frame_average_brightness, uint32_t min_brightness) {
+ (void)device; (void)max_frame_average_brightness; (void)min_brightness;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->max_peak_brightness = max_peak_brightness;
+}
+
+static void kde_output_device_handle_brightness_overrides(void *data, struct kde_output_device_v2 *device, int32_t max_peak_brightness, int32_t max_average_brightness, int32_t min_brightness) {
+ (void)device; (void)max_average_brightness; (void)min_brightness;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->max_peak_brightness_override = max_peak_brightness;
+}
+
+static void kde_output_device_handle_brightness(void *data, struct kde_output_device_v2 *device, uint32_t brightness) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->brightness = brightness;
+}
+
+static void kde_output_device_handle_dimming(void *data, struct kde_output_device_v2 *device, uint32_t multiplier) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ gsr_device->dimming = multiplier;
+}
+
+static void kde_output_device_handle_max_bits_per_color_range(void *data, struct kde_output_device_v2 *device, uint32_t min_value, uint32_t max_value) {
+ (void)data; (void)device; (void)min_value; (void)max_value;
+}
+
+static void kde_output_device_handle_removed(void *data, struct kde_output_device_v2 *device) {
+ (void)device;
+ gsr_kde_output_device *gsr_device = data;
+ kde_output_device_free(gsr_device);
+}
+
+static const struct kde_output_device_v2_listener kde_output_device_listener = {
+ .geometry = kde_output_device_handle_geometry,
+ .current_mode = kde_output_device_handle_current_mode,
+ .mode = kde_output_device_handle_mode,
+ .done = kde_output_device_handle_done,
+ .scale = kde_output_device_handle_scale,
+ .edid = kde_output_device_handle_edid,
+ .enabled = kde_output_device_handle_enabled,
+ .uuid = kde_output_device_handle_string_noop,
+ .serial_number = kde_output_device_handle_string_noop,
+ .eisa_id = kde_output_device_handle_string_noop,
+ .capabilities = kde_output_device_handle_uint_noop,
+ .overscan = kde_output_device_handle_uint_noop,
+ .vrr_policy = kde_output_device_handle_uint_noop,
+ .rgb_range = kde_output_device_handle_uint_noop,
+ .name = kde_output_device_handle_name,
+ .high_dynamic_range = kde_output_device_handle_high_dynamic_range,
+ .sdr_brightness = kde_output_device_handle_sdr_brightness,
+ .wide_color_gamut = kde_output_device_handle_uint_noop,
+ .auto_rotate_policy = kde_output_device_handle_uint_noop,
+ .icc_profile_path = kde_output_device_handle_string_noop,
+ .brightness_metadata = kde_output_device_handle_brightness_metadata,
+ .brightness_overrides = kde_output_device_handle_brightness_overrides,
+ .sdr_gamut_wideness = kde_output_device_handle_uint_noop,
+ .color_profile_source = kde_output_device_handle_uint_noop,
+ .brightness = kde_output_device_handle_brightness,
+ .color_power_tradeoff = kde_output_device_handle_uint_noop,
+ .dimming = kde_output_device_handle_dimming,
+ .replication_source = kde_output_device_handle_string_noop,
+ .ddc_ci_allowed = kde_output_device_handle_uint_noop,
+ .max_bits_per_color = kde_output_device_handle_uint_noop,
+ .max_bits_per_color_range = kde_output_device_handle_max_bits_per_color_range,
+ .automatic_max_bits_per_color_limit = kde_output_device_handle_uint_noop,
+ .edr_policy = kde_output_device_handle_uint_noop,
+ .sharpness = kde_output_device_handle_uint_noop,
+ .priority = kde_output_device_handle_uint_noop,
+ .auto_brightness = kde_output_device_handle_uint_noop,
+ .removed = kde_output_device_handle_removed,
+ .hdr_icc_profile_path = kde_output_device_handle_string_noop,
+ .hdr_color_profile_source = kde_output_device_handle_uint_noop,
+ .abm_level = kde_output_device_handle_uint_noop,
+};
+
+static void gsr_window_wayland_add_kde_output_device(gsr_window_wayland *self, struct kde_output_device_v2 *device) {
+ gsr_kde_output_device *gsr_device = NULL;
+ for(int i = 0; i < GSR_MAX_OUTPUTS; ++i) {
+ if(!self->kde_output_devices[i].device) {
+ gsr_device = &self->kde_output_devices[i];
+ break;
+ }
+ }
+
+ if(!gsr_device) {
+ fprintf(stderr, "gsr warning: gsr_window_wayland_add_kde_output_device: reached maximum outputs (%d), ignoring output\n", GSR_MAX_OUTPUTS);
+ kde_output_device_v2_destroy(device);
+ return;
+ }
+
+ if(gsr_device->name) {
+ free(gsr_device->name);
+ gsr_device->name = NULL;
+ }
+
+ *gsr_device = (gsr_kde_output_device) {
+ .device = device,
+ .name = NULL,
+ .hdr_enabled = false,
+ .sdr_brightness = 0,
+ .max_peak_brightness = 0,
+ .max_peak_brightness_override = -1,
+ .brightness = KDE_BRIGHTNESS_MULTIPLIER_MAX,
+ .dimming = KDE_BRIGHTNESS_MULTIPLIER_MAX,
+ };
+ kde_output_device_v2_add_listener(device, &kde_output_device_listener, gsr_device);
+}
+
+static void kde_output_device_registry_handle_output(void *data, struct kde_output_device_registry_v2 *registry, struct kde_output_device_v2 *output) {
+ (void)registry;
+ gsr_window_wayland *self = data;
+ gsr_window_wayland_add_kde_output_device(self, output);
+}
+
+static void kde_output_device_registry_handle_finished(void *data, struct kde_output_device_registry_v2 *registry) {
+ (void)registry;
+ gsr_window_wayland *self = data;
+ if(self->kde_output_device_registry) {
+ kde_output_device_registry_v2_destroy(self->kde_output_device_registry);
+ self->kde_output_device_registry = NULL;
+ }
+}
+
+static const struct kde_output_device_registry_v2_listener kde_output_device_registry_listener = {
+ .finished = kde_output_device_registry_handle_finished,
+ .output = kde_output_device_registry_handle_output,
+};
+
static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
(void)version;
gsr_window_wayland *window_wayland = data;
@@ -140,6 +359,26 @@ static void registry_add_object(void *data, struct wl_registry *registry, uint32
return;
window_wayland->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
+ } else if(strcmp(interface, kde_output_device_registry_v2_interface.name) == 0) {
+ if(window_wayland->kde_output_device_registry)
+ return;
+
+ uint32_t bind_version = version;
+ if(bind_version > (uint32_t)kde_output_device_registry_v2_interface.version)
+ bind_version = kde_output_device_registry_v2_interface.version;
+
+ window_wayland->kde_output_device_registry = wl_registry_bind(registry, name, &kde_output_device_registry_v2_interface, bind_version);
+ kde_output_device_registry_v2_add_listener(window_wayland->kde_output_device_registry, &kde_output_device_registry_listener, window_wayland);
+ } else if(strcmp(interface, kde_output_device_v2_interface.name) == 0) {
+ if(version < 3)
+ return;
+
+ uint32_t bind_version = version;
+ if(bind_version > (uint32_t)kde_output_device_v2_interface.version)
+ bind_version = kde_output_device_v2_interface.version;
+
+ struct kde_output_device_v2 *device = wl_registry_bind(registry, name, &kde_output_device_v2_interface, bind_version);
+ gsr_window_wayland_add_kde_output_device(window_wayland, device);
}
}
@@ -274,6 +513,15 @@ static void gsr_window_wayland_deinit(gsr_window_wayland *self) {
}
self->num_outputs = 0;
+ for(int i = 0; i < GSR_MAX_OUTPUTS; ++i) {
+ kde_output_device_free(&self->kde_output_devices[i]);
+ }
+
+ if(self->kde_output_device_registry) {
+ kde_output_device_registry_v2_destroy(self->kde_output_device_registry);
+ self->kde_output_device_registry = NULL;
+ }
+
if(self->xdg_output_manager) {
zxdg_output_manager_v1_destroy(self->xdg_output_manager);
self->xdg_output_manager = NULL;
@@ -420,6 +668,27 @@ static void gsr_window_wayland_for_each_active_monitor_output_cached(const gsr_w
}
}
+static bool gsr_window_wayland_get_monitor_hdr_info(const gsr_window *window, const char *monitor_name, gsr_monitor_hdr_info *hdr_info) {
+ const gsr_window_wayland *self = window->priv;
+ for(int i = 0; i < GSR_MAX_OUTPUTS; ++i) {
+ const gsr_kde_output_device *device = &self->kde_output_devices[i];
+ if(!device->device || !device->name || strcmp(device->name, monitor_name) != 0)
+ continue;
+
+ if(device->sdr_brightness == 0)
+ return false;
+
+ const float brightness_multiplier = ((float)device->brightness / (float)KDE_BRIGHTNESS_MULTIPLIER_MAX) * ((float)device->dimming / (float)KDE_BRIGHTNESS_MULTIPLIER_MAX);
+ hdr_info->hdr_enabled = device->hdr_enabled;
+ hdr_info->sdr_white_luminance = (float)device->sdr_brightness * brightness_multiplier;
+ hdr_info->max_peak_luminance = device->max_peak_brightness_override > 0 ? (float)device->max_peak_brightness_override : (float)device->max_peak_brightness;
+ if(hdr_info->max_peak_luminance <= 0.0f)
+ hdr_info->max_peak_luminance = KDE_PLASMA_ASSUMED_MONITOR_PEAK_LUMINANCE;
+ return true;
+ }
+ return false;
+}
+
gsr_window* gsr_window_wayland_create(void) {
gsr_window *window = calloc(1, sizeof(gsr_window));
if(!window)
@@ -445,6 +714,7 @@ gsr_window* gsr_window_wayland_create(void) {
.get_display = gsr_window_wayland_get_display,
.get_window = gsr_window_wayland_get_window,
.for_each_active_monitor_output_cached = gsr_window_wayland_for_each_active_monitor_output_cached,
+ .get_monitor_hdr_info = gsr_window_wayland_get_monitor_hdr_info,
.priv = window_wayland
};
diff --git a/src/window/window.c b/src/window/window.c
index 1c6a24e..5ede9fd 100644
--- a/src/window/window.c
+++ b/src/window/window.c
@@ -28,3 +28,9 @@ void* gsr_window_get_window(gsr_window *self) {
void gsr_window_for_each_active_monitor_output_cached(const gsr_window *self, active_monitor_callback callback, void *userdata) {
self->for_each_active_monitor_output_cached(self, callback, userdata);
}
+
+bool gsr_window_get_monitor_hdr_info(const gsr_window *self, const char *monitor_name, gsr_monitor_hdr_info *hdr_info) {
+ if(self->get_monitor_hdr_info)
+ return self->get_monitor_hdr_info(self, monitor_name, hdr_info);
+ return false;
+}