diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | TODO | 9 | ||||
| -rw-r--r-- | gpu-screen-recorder.1 | 10 | ||||
| -rw-r--r-- | include/args_parser.h | 3 | ||||
| -rw-r--r-- | include/capture/kms.h | 2 | ||||
| -rw-r--r-- | include/color_conversion.h | 29 | ||||
| -rw-r--r-- | include/defs.h | 5 | ||||
| -rw-r--r-- | include/egl.h | 2 | ||||
| -rw-r--r-- | include/kde_night_light.h | 13 | ||||
| -rw-r--r-- | include/sound.hpp | 2 | ||||
| -rw-r--r-- | include/window/window.h | 2 | ||||
| -rw-r--r-- | meson.build | 4 | ||||
| -rw-r--r-- | project.conf | 6 | ||||
| -rw-r--r-- | protocol/color-management-v1.xml | 1756 | ||||
| -rw-r--r-- | protocol/meson.build | 1 | ||||
| -rw-r--r-- | src/args_parser.c | 4 | ||||
| -rw-r--r-- | src/capture/kms.c | 294 | ||||
| -rw-r--r-- | src/capture/nvfbc.c | 5 | ||||
| -rw-r--r-- | src/capture/xcomposite.c | 5 | ||||
| -rw-r--r-- | src/color_conversion.c | 387 | ||||
| -rw-r--r-- | src/egl.c | 1 | ||||
| -rw-r--r-- | src/image_writer.c | 58 | ||||
| -rw-r--r-- | src/kde_night_light.c | 213 | ||||
| -rw-r--r-- | src/main.cpp | 29 | ||||
| -rw-r--r-- | src/utils.c | 6 | ||||
| -rw-r--r-- | src/window/wayland.c | 176 | ||||
| -rw-r--r-- | src/window/window.c | 6 |
28 files changed, 2981 insertions, 64 deletions
@@ -7,6 +7,9 @@ tests/compile_commands.json **/xdg-output-unstable-v1-client-protocol.h **/xdg-output-unstable-v1-protocol.c +**/color-management-v1-client-protocol.h +**/color-management-v1-protocol.c + .clangd/ .cache/ .vscode/ @@ -14,7 +14,7 @@ This is a cli-only tool, if you want an UI for this check out [GPU Screen Record Supported video codecs: * H264 (default) * HEVC (Optionally with HDR) -* AV1 (Optionally with HDR. Not currently supported on NVIDIA in the flatpak version of GPU Screen Recorder) +* AV1 * VP8 * VP9 @@ -78,7 +78,7 @@ When building GPU Screen Recorder with portal support (`-Dportal=true` meson opt ## Runtime dependencies * libglvnd (which provides libgl, libglx and libegl) is needed. Your system needs to support at least OpenGL ES 3.0 (released in 2012) * vulkan-icd-loader (which provides the runtime vulkan library. This is only needed if vulkan video encoding option is used) -* libturbojpeg (aka libjpeg-turbo) is needed when capturing camera with mjpeg pixel format option +* libturbojpeg (aka libjpeg-turbo) is needed when capturing camera with mjpeg pixel format option, or for faster jpeg screenshots There are also additional dependencies needed at runtime depending on your GPU vendor: @@ -236,15 +236,11 @@ 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 -You can record with desktop portal option (`-w portal`) instead which ignores night light, if you are ok with recording without HDR. +## GPU Screen Recorder records night light +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). -## 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? @@ -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. @@ -424,3 +422,10 @@ Add the option to use application audio with exectable name, or automatically ch In the ui it should also list the application name beside the app audio. Region capture works incorrectly on gnome with hidpi, especially with 150% scaling. Same in gsr ui. + +-s works incorrectly with multiple capture sources, for example "screen|screen;x=3840" -s 3840x2160. + To fix this the capture methods need to store the original size of the capture source first, then loop through all capture sources and rescale them. Do the rescaling outside the capture methods code? + +Take color pipeline into consideration in kms hdr capture. Right now kde only uses that on intel. + +Maybe use 3D LUT for hdr tonemapping instead of per fragment math.
\ No newline at end of file diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 564d35b..38d4df6 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -219,9 +219,12 @@ Container format (mp4, mkv, flv, webm). Defaults to extension from filename. .TP .BI \-s " WxH" -Output resolution limit (e.g., 1920x1080). Use 0x0 for original resolution. +Output resolution limit (e.g., 1920x1080). The captured content is scaled to fit within this +resolution while keeping its aspect ratio and the video size shrinks to fit the scaled content. +Use 0x0 for original resolution. Required for -.BR \-w " focused." +.BR \-w " focused" +(where the video is exactly this resolution). .TP .BI \-f " fps" Frame rate (default: 60). For variable frame rate mode, this is maximum FPS. @@ -379,6 +382,9 @@ when this is set to .B yes to only encode frames when the screen content updates to lower GPU and video encoding usage when the system is idle. .TP +.BI \-exclude-metadata " yes|no" +Exclude personal metadata from the video file, such as audio track title that is composed from the name of the audio device by default (default: no). +.TP .BI \-write\-first\-frame\-ts " yes|no" When enabled, writes a timestamp file with extra extension \fI.ts\fR next to the output video containing: .nf diff --git a/include/args_parser.h b/include/args_parser.h index 2208a9a..e8c68d8 100644 --- a/include/args_parser.h +++ b/include/args_parser.h @@ -8,7 +8,7 @@ typedef struct gsr_egl gsr_egl; -#define NUM_ARGS 37 +#define NUM_ARGS 38 typedef enum { GSR_CAPTURE_SOURCE_TYPE_WINDOW, @@ -93,6 +93,7 @@ typedef struct { bool gl_debug; bool fallback_cpu_encoding; bool low_power; + bool exclude_metadata; bool record_cursor; bool date_folders; bool restore_portal_session; diff --git a/include/capture/kms.h b/include/capture/kms.h index deb12a0..5c4eae4 100644 --- a/include/capture/kms.h +++ b/include/capture/kms.h @@ -3,12 +3,14 @@ #include "capture.h" #include "../cursor.h" +#include "../kde_night_light.h" #include "../../kms/kms_shared.h" typedef struct { gsr_egl *egl; gsr_cursor *x11_cursor; gsr_kms_response *kms_response; + gsr_kde_night_light *kde_night_light; /* optional */ const char *display_to_capture; /* A copy is made of this */ bool hdr; bool record_cursor; diff --git a/include/color_conversion.h b/include/color_conversion.h index f1f1b29..469dea9 100644 --- a/include/color_conversion.h +++ b/include/color_conversion.h @@ -6,10 +6,16 @@ #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 { + GSR_COLOR_MATRIX_TRANSFER_PQ = 1, + GSR_COLOR_MATRIX_TRANSFER_GAMMA22 = 2, + GSR_COLOR_MATRIX_TRANSFER_GAMMA22_TO_PQ = 3 +} gsr_color_matrix_transfer; + +typedef enum { GSR_SOURCE_COLOR_RGB, GSR_SOURCE_COLOR_BGR, GSR_SOURCE_COLOR_YUYV @@ -37,6 +43,12 @@ 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; + int color_matrix_transfer; + int color_matrix; } gsr_color_graphics_uniforms; typedef struct { @@ -64,6 +76,15 @@ 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; + int color_matrix_transfer; + float color_matrix[9]; + 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 +92,12 @@ 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); +/* |linear_rgb_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. + |transfer| chooses the transfer characteristics used to decode/encode the source image around the matrix multiply */ +void gsr_color_conversion_set_color_matrix(gsr_color_conversion *self, const float *linear_rgb_matrix, gsr_color_matrix_transfer 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/defs.h b/include/defs.h index d6de6d8..b868148 100644 --- a/include/defs.h +++ b/include/defs.h @@ -26,6 +26,11 @@ typedef enum { GSR_MONITOR_ROT_270, } gsr_monitor_rotation; +typedef struct { + 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..c8dcd2b 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 @@ -310,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/include/sound.hpp b/include/sound.hpp index 6d903d8..d78e4b6 100644 --- a/include/sound.hpp +++ b/include/sound.hpp @@ -18,6 +18,7 @@ #ifndef GPU_SCREEN_RECORDER_H #define GPU_SCREEN_RECORDER_H +#include <optional> #include <vector> #include <string> @@ -49,6 +50,7 @@ struct AudioInput { }; struct MergedAudioInputs { + std::optional<std::string> custom_name; std::string track_name; std::vector<AudioInput> audio_inputs; }; 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/meson.build b/meson.build index 6865bbd..4b24f75 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gpu-screen-recorder', ['c', 'cpp'], version : '5.14.1', default_options : ['warning_level=2']) +project('gpu-screen-recorder', ['c', 'cpp'], version : '5.15.1', default_options : ['warning_level=2', 'cpp_std=c++17']) add_project_arguments('-Wshadow', language : ['c', 'cpp']) if get_option('buildtype') == 'debug' @@ -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 40ae00e..26b995e 100644 --- a/project.conf +++ b/project.conf @@ -1,9 +1,12 @@ [package] name = "gpu-screen-recorder" type = "executable" -version = "5.14.1" +version = "5.15.1" platforms = ["posix"] +[lang.cpp] +version = "c++17" + [config] ignore_dirs = ["kms/server", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" @@ -11,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/protocol/color-management-v1.xml b/protocol/color-management-v1.xml new file mode 100644 index 0000000..b47e108 --- /dev/null +++ b/protocol/color-management-v1.xml @@ -0,0 +1,1756 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="color_management_v1"> + <copyright> + Copyright 2019 Sebastian Wick + Copyright 2019 Erwin Burema + Copyright 2020 AMD + Copyright 2020-2024 Collabora, Ltd. + Copyright 2024 Xaver Hugl + Copyright 2022-2025 Red Hat, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <description summary="color management protocol"> + The aim of the color management extension is to allow clients to know + the color properties of outputs, and to tell the compositor about the color + properties of their content on surfaces. All surface contents must be + readily intended for some display, but not necessarily for the display at + hand. Doing this enables a compositor to perform automatic color management + of content for different outputs according to how content is intended to + look like. + + For an introduction, see the section "Color management" in the Wayland + documentation at https://wayland.freedesktop.org/docs/html/ . + + The color properties are represented as an image description object which + is immutable after it has been created. A wl_output always has an + associated image description that clients can observe. A wl_surface + always has an associated preferred image description as a hint chosen by + the compositor that clients can also observe. Clients can set an image + description on a wl_surface to denote the color characteristics of the + surface contents. + + An image description essentially defines a display and (indirectly) its + viewing environment. An image description includes SDR and HDR colorimetry + and encoding, HDR metadata, and some parameters related to the viewing + environment. An image description does not include the properties set + through color-representation extension. It is expected that the + color-representation extension is used in conjunction with the + color-management extension when necessary, particularly with the YUV family + of pixel formats. + + The normative appendix for this protocol is in the appendix.md file beside + this XML file. + + The color-and-hdr repository + (https://gitlab.freedesktop.org/pq/color-and-hdr) contains + background information on the protocol design and legacy color management. + It also contains a glossary, learning resources for digital color, tools, + samples and more. + + The terminology used in this protocol is based on common color science and + color encoding terminology where possible. The glossary in the color-and-hdr + repository shall be the authority on the definition of terms in this + protocol. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + </description> + + <interface name="wp_color_manager_v1" version="3"> + <description summary="color manager singleton"> + A singleton global interface used for getting color management extensions + for wl_surface and wl_output objects, and for creating client defined + image description objects. The extension interfaces allow + getting the image description of outputs and setting the image + description of surfaces. + + Compositors should never remove this global. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the color manager"> + Destroy the wp_color_manager_v1 object. This does not affect any other + objects in any way. + </description> + </request> + + <enum name="error"> + <entry name="unsupported_feature" value="0" + summary="request not supported"/> + <entry name="surface_exists" value="1" + summary="color management surface exists already"/> + </enum> + + <enum name="render_intent"> + <description summary="rendering intents"> + See the ICC.1:2022 specification from the International Color Consortium + for more details about rendering intents. + + The principles of ICC defined rendering intents apply with all types of + image descriptions, not only those with ICC file profiles. + + Compositors must support the perceptual rendering intent. Other + rendering intents are optional. + </description> + + <entry name="perceptual" value="0" + summary="perceptual"/> + <entry name="relative" value="1" + summary="media-relative colorimetric"/> + <entry name="saturation" value="2" + summary="saturation"/> + <entry name="absolute" value="3" + summary="ICC-absolute colorimetric"/> + <entry name="relative_bpc" value="4" + summary="media-relative colorimetric + black point compensation"/> + <entry name="absolute_no_adaptation" value="5" since="2"> + <description summary="ICC-absolute colorimetric without adaptation"> + This rendering intent is a modified absolute rendering intent that + assumes the viewer is not adapted to the display white point, so no + chromatic adaptation between surface and display is done. + This can be useful for color proofing applications. + </description> + </entry> + </enum> + + <enum name="feature"> + <description summary="compositor supported features"/> + + <entry name="icc_v2_v4" value="0" + summary="create_icc_creator request"/> + <entry name="parametric" value="1" + summary="create_parametric_creator request"/> + <entry name="set_primaries" value="2" + summary="parametric set_primaries request"/> + <entry name="set_tf_power" value="3" + summary="parametric set_tf_power request"/> + <entry name="set_luminances" value="4" + summary="parametric set_luminances request"/> + <entry name="set_mastering_display_primaries" value="5"> + <description summary="parametric set_mastering_display_primaries request"> + The compositor supports set_mastering_display_primaries request with a + target color volume fully contained inside the primary color volume. + </description> + </entry> + <entry name="extended_target_volume" value="6"> + <description summary="parametric target exceeds primary color volume"> + The compositor additionally supports target color volumes that + extend outside of the primary color volume. + + This can only be advertised if feature set_mastering_display_primaries + is supported as well. + </description> + </entry> + <entry name="windows_scrgb" value="7" + summary="create_windows_scrgb request"/> + <entry name="windows_bt2100" value="8" + summary="create_windows_bt2100 request"/> + </enum> + + <enum name="primaries"> + <description summary="named color primaries"> + Named color primaries used to encode well-known sets of primaries. + + A value of 0 is invalid and will never be present in the list of enums. + </description> + + <entry name="srgb" value="1"> + <description summary="Color primaries for the sRGB color space as defined by the BT.709 standard"> + Color primaries as defined by + - Rec. ITU-R BT.709-6 + - Rec. ITU-R BT.1361-0 conventional colour gamut system and extended + colour gamut system (historical) + - IEC 61966-2-1 sRGB or sYCC + - IEC 61966-2-4 + - Society of Motion Picture and Television Engineers (SMPTE) RP 177 + (1993) Annex B + </description> + </entry> + <entry name="pal_m" value="2"> + <description summary="Color primaries for PAL-M as defined by the BT.470 standard"> + Color primaries as defined by + - Rec. ITU-R BT.470-6 System M (historical) + - United States National Television System Committee 1953 + Recommendation for transmission standards for color television + - United States Federal Communications Commission (2003) Title 47 Code + of Federal Regulations 73.682 (a)(20) + </description> + </entry> + <entry name="pal" value="3"> + <description summary="Color primaries for PAL as defined by the BT.601 standard"> + Color primaries as defined by + - Rec. ITU-R BT.470-6 System B, G (historical) + - Rec. ITU-R BT.601-7 625 + - Rec. ITU-R BT.1358-0 625 (historical) + - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + </description> + </entry> + <entry name="ntsc" value="4"> + <description summary="Color primaries for NTSC as defined by the BT.601 standard"> + Color primaries as defined by + - Rec. ITU-R BT.601-7 525 + - Rec. ITU-R BT.1358-1 525 or 625 (historical) + - Rec. ITU-R BT.1700-0 NTSC + - SMPTE 170M (2004) + - SMPTE 240M (1999) (historical) + </description> + </entry> + <entry name="generic_film" value="5"> + <description summary="Generic film with colour filters using Illuminant C"> + Color primaries as defined by Recommendation ITU-T H.273 + "Coding-independent code points for video signal type identification" + for "generic film". + </description> + </entry> + <entry name="bt2020" value="6"> + <description summary="Color primaries as defined by the BT.2020 and BT.2100 standard"> + Color primaries as defined by + - Rec. ITU-R BT.2020-2 + - Rec. ITU-R BT.2100-0 + </description> + </entry> + <entry name="cie1931_xyz" value="7"> + <description summary="Color primaries of the full CIE 1931 XYZ color space"> + Color primaries as defined as the maximum of the CIE 1931 XYZ color + space by + - SMPTE ST 428-1 + - (CIE 1931 XYZ as in ISO 11664-1) + </description> + </entry> + <entry name="dci_p3" value="8"> + <description summary="Color primaries of the DCI P3 color space as defined by the SMPTE RP 431 standard"> + Color primaries as defined by Digital Cinema System and published in + SMPTE RP 431-2 (2011). + </description> + </entry> + <entry name="display_p3" value="9"> + <description summary="Color primaries of Display P3 variant of the DCI-P3 color space as defined by the SMPTE EG 432 standard"> + Color primaries as defined by Digital Cinema System and published in + SMPTE EG 432-1 (2010). + </description> + </entry> + <entry name="adobe_rgb" value="10"> + <description summary="Color primaries of the Adobe RGB color space as defined by the ISO 12640 standard"> + Color primaries as defined by Adobe as "Adobe RGB" and later published + by ISO 12640-4 (2011). + </description> + </entry> + </enum> + + <enum name="transfer_function"> + <description summary="named transfer functions"> + Named transfer functions used to represent well-known transfer + characteristics of displays. + + A value of 0 is invalid and will never be present in the list of enums. + + See appendix.md for the formulae. + </description> + + <entry name="bt1886" value="1"> + <description summary="BT.1886 display transfer characteristic"> + Rec. ITU-R BT.1886 is the display transfer characteristic assumed by + - Rec. ITU-R BT.601-7 525 and 625 + - Rec. ITU-R BT.709-6 + - Rec. ITU-R BT.2020-2 + + This TF implies these default luminances from Rec. ITU-R BT.2035: + - primary color volume minimum: 0.01 cd/m² + - primary color volume maximum: 100 cd/m² + - reference white: 100 cd/m² + </description> + </entry> + <entry name="gamma22" value="2"> + <description summary="Assumed display gamma 2.2 transfer function"> + Transfer characteristics as defined by + - Rec. ITU-R BT.470-6 System M (historical) + - United States National Television System Committee 1953 + Recommendation for transmission standards for color television + - United States Federal Communications Commission (2003) Title 47 Code + of Federal Regulations 73.682 (a) (20) + - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + - IEC 61966-2-1 (reference display) + </description> + </entry> + <entry name="gamma28" value="3"> + <description summary="Assumed display gamma 2.8 transfer function"> + Transfer characteristics as defined by + - Rec. ITU-R BT.470-6 System B, G (historical) + </description> + </entry> + <entry name="st240" value="4"> + <description summary="SMPTE ST 240 transfer function"> + Transfer characteristics as defined by + - SMPTE ST 240 (1999) + </description> + </entry> + <entry name="ext_linear" value="5"> + <description summary="extended linear transfer function"> + Linear transfer function defined over all real numbers. + Normalised electrical values are equal the normalised optical values. + </description> + </entry> + <entry name="log_100" value="6"> + <description summary="logarithmic 100:1 transfer function"> + Logarithmic transfer characteristic (100:1 range). + </description> + </entry> + <entry name="log_316" value="7"> + <description summary="logarithmic (100*Sqrt(10) : 1) transfer function"> + Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range). + </description> + </entry> + <entry name="xvycc" value="8"> + <description summary="IEC 61966-2-4 transfer function"> + Transfer characteristics as defined by + - IEC 61966-2-4 + </description> + </entry> + <entry name="srgb" value="9" deprecated-since="2"> + <description summary="Deprecated (ambiguous sRGB transfer function)"> + Transfer characteristics as defined by + - IEC 61966-2-1 sRGB + + As a rule of thumb, use gamma22 for video, motion picture and + computer graphics, or compound_power_2_4 for ICC calibrated print + workflows. + </description> + </entry> + <entry name="ext_srgb" value="10" deprecated-since="2"> + <description summary="Deprecated (Extended sRGB piece-wise transfer function)"> + Transfer characteristics as defined by + - IEC 61966-2-1 sYCC + </description> + </entry> + <entry name="st2084_pq" value="11"> + <description summary="perceptual quantizer transfer function"> + Transfer characteristics as defined by + - SMPTE ST 2084 (2014) for 10-, 12-, 14- and 16-bit systems + - Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system + + This TF implies these default luminances + - primary color volume minimum: 0.005 cd/m² + - primary color volume maximum: 10000 cd/m² + - reference white: 203 cd/m² + + The difference between the primary color volume minimum and maximum + must be approximately 10000 cd/m² as that is the swing of the EOTF + defined by ST 2084 and BT.2100. The default value for the + reference white is a protocol addition: it is suggested by + Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100. + </description> + </entry> + <entry name="st428" value="12"> + <description summary="SMPTE ST 428 transfer function"> + Transfer characteristics as defined by + - SMPTE ST 428-1 (2019) + </description> + </entry> + <entry name="hlg" value="13"> + <description summary="hybrid log-gamma transfer function"> + Transfer characteristics as defined by + - ARIB STD-B67 (2015) + - Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system + + This TF implies these default luminances + - primary color volume minimum: 0.005 cd/m² + - primary color volume maximum: 1000 cd/m² + - reference white: 203 cd/m² + + HLG is a relative display-referred signal with a specified + non-linear mapping to the display peak luminance (the HLG OOTF). + All absolute luminance values used here for HLG assume a 1000 cd/m² + peak display. + + The default value for the reference white is a protocol addition: + it is suggested by Report ITU-R BT.2408-7 and is not part of + ARIB STD-B67 or BT.2100. + </description> + </entry> + <entry name="compound_power_2_4" value="14" since="2"> + <description summary="IEC 61966-2-1 encoding function"> + Encoding characteristics as defined by IEC 61966-2-1, for displays + that invert the encoding function. + </description> + </entry> + </enum> + + <request name="get_output"> + <description summary="create a color management interface for a wl_output"> + This creates a new wp_color_management_output_v1 object for the + given wl_output. + + See the wp_color_management_output_v1 interface for more details. + </description> + + <arg name="id" type="new_id" interface="wp_color_management_output_v1"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + + <request name="get_surface"> + <description summary="create a color management interface for a wl_surface"> + If a wp_color_management_surface_v1 object already exists for the given + wl_surface, the protocol error surface_exists is raised. + + This creates a new color wp_color_management_surface_v1 object for the + given wl_surface. + + See the wp_color_management_surface_v1 interface for more details. + </description> + + <arg name="id" type="new_id" interface="wp_color_management_surface_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + </request> + + <request name="get_surface_feedback"> + <description summary="create a color management feedback interface"> + This creates a new color wp_color_management_surface_feedback_v1 object + for the given wl_surface. + + See the wp_color_management_surface_feedback_v1 interface for more + details. + </description> + + <arg name="id" type="new_id" + interface="wp_color_management_surface_feedback_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + </request> + + <request name="create_icc_creator"> + <description summary="make a new ICC-based image description creator object"> + Makes a new ICC-based image description creator object with all + properties initially unset. The client can then use the object's + interface to define all the required properties for an image description + and finally create a wp_image_description_v1 object. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.icc_v2_v4. + Otherwise this request raises the protocol error unsupported_feature. + </description> + + <arg name="obj" + type="new_id" interface="wp_image_description_creator_icc_v1" + summary="the new creator object"/> + </request> + + <request name="create_parametric_creator"> + <description summary="make a new parametric image description creator object"> + Makes a new parametric image description creator object with all + properties initially unset. The client can then use the object's + interface to define all the required properties for an image description + and finally create a wp_image_description_v1 object. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.parametric. + Otherwise this request raises the protocol error unsupported_feature. + </description> + + <arg name="obj" + type="new_id" interface="wp_image_description_creator_params_v1" + summary="the new creator object"/> + </request> + + <request name="create_windows_scrgb"> + <description summary="create Windows-scRGB image description object"> + This creates a pre-defined image description for the so-called + Windows-scRGB stimulus encoding. This comes from the Windows 10 handling + of its own definition of an scRGB color space for an HDR screen + driven in BT.2100/PQ signalling mode. + + Windows-scRGB uses sRGB (BT.709) color primaries and white point. + The transfer characteristic is extended linear. + + The nominal color channel value range is extended, meaning it includes + negative and greater than 1.0 values. Negative values are used to + escape the sRGB color gamut boundaries. To make use of the extended + range, the client needs to use a pixel format that can represent those + values, e.g. floating-point 16 bits per channel. + + Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system + 0 cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 cd/m². + The maximum is R=G=B=125.0 corresponding to 10k cd/m². + + Windows-scRGB is displayed by Windows 10 by converting it to + BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping the + luminance as above. No adjustment is made to the signal to account + for the viewing conditions. + + The reference white level of Windows-scRGB is unknown. If a + reference white level must be assumed for compositor processing, it + should be R=G=B=2.5375 corresponding to 203 cd/m² of Report ITU-R + BT.2408-7. + + The target color volume of Windows-scRGB is unknown. The color gamut + may be anything between sRGB and BT.2100. + + Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from + Windows-scRGB by using R=G=B=1.0 as the reference white level, while + Windows-scRGB reference white level is unknown or varies. However, + it seems probable that Windows implements both + EGL_EXT_gl_colorspace_scrgb_linear and Vulkan + VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.windows_scrgb. + Otherwise this request raises the protocol error unsupported_feature. + + The resulting image description object does not allow get_information + request. The wp_image_description_v1.ready event shall be sent. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + + <event name="supported_intent"> + <description summary="supported rendering intent"> + When this object is created, it shall immediately send this event once + for each rendering intent the compositor supports. + + A compositor must not advertise intents that are deprecated in the + bound version of the interface. + </description> + + <arg name="render_intent" type="uint" enum="render_intent" + summary="rendering intent"/> + </event> + + <event name="supported_feature"> + <description summary="supported features"> + When this object is created, it shall immediately send this event once + for each compositor supported feature listed in the enumeration. + + A compositor must not advertise features that are deprecated in the + bound version of the interface. + </description> + + <arg name="feature" type="uint" enum="feature" + summary="supported feature"/> + </event> + + <event name="supported_tf_named"> + <description summary="supported named transfer characteristic"> + When this object is created, it shall immediately send this event once + for each named transfer function the compositor supports with the + parametric image description creator. + + A compositor must not advertise transfer functions that are deprecated + in the bound version of the interface. + </description> + + <arg name="tf" type="uint" enum="transfer_function" + summary="Named transfer function"/> + </event> + + <event name="supported_primaries_named"> + <description summary="supported named primaries"> + When this object is created, it shall immediately send this event once + for each named set of primaries the compositor supports with the + parametric image description creator. + + A compositor must not advertise names that are deprecated in the + bound version of the interface. + </description> + + <arg name="primaries" type="uint" enum="primaries" + summary="Named color primaries"/> + </event> + + <event name="done"> + <description summary="all features have been sent"> + This event is sent when all supported rendering intents, features, + transfer functions and named primaries have been sent. + </description> + </event> + + <request name="get_image_description" since="2"> + <description summary="create an image description from a reference"> + This request retrieves the image description backing a reference. + + The get_information request can be used if and only if the request that + creates the reference allows it. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + <arg name="reference" + type="object" interface="wp_image_description_reference_v1"/> + </request> + + <request name="create_windows_bt2100" since="3"> + <description summary="create Windows-BT.2100 image description object"> + This creates a pre-defined image description for the so-called + Windows-BT.2100 stimulus encoding. This comes from the Windows 10 + handling of its own definition of a BT.2100 color space for an HDR + screen driven in BT.2100/PQ signalling mode. + + Windows-BT.2100 uses BT.2020 color primaries and white point. + The transfer characteristic is st2084_pq. + + Windows-BT.2100 is generally displayed by Windows 10 without any + adjustments to the signal to account for viewing conditions. + + The reference white level of Windows-BT.2100 is unknown. If a + reference white level must be assumed for compositor processing, it + should be 203 cd/m² of Report ITU-R BT.2408-7. + + The target color volume of Windows-BT.2100 is unknown. The color gamut + may be anything up to BT.2100. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.windows_bt2100. + Otherwise this request raises the protocol error unsupported_feature. + + The resulting image description object does not allow get_information + request. The wp_image_description_v1.ready event shall be sent. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + </interface> + + <interface name="wp_color_management_output_v1" version="3"> + <description summary="output color properties"> + A wp_color_management_output_v1 describes the color properties of an + output. + + The wp_color_management_output_v1 is associated with the wl_output global + underlying the wl_output object. Therefore the client destroying the + wl_output object has no impact, but the compositor removing the output + global makes the wp_color_management_output_v1 object inert. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the color management output"> + Destroy the color wp_color_management_output_v1 object. This does not + affect any remaining protocol objects. + </description> + </request> + + <event name="image_description_changed"> + <description summary="image description changed"> + This event is sent whenever the image description of the output changed, + followed by one wl_output.done event common to output events across all + extensions. + + If the client wants to use the updated image description, it needs to do + get_image_description again, because image description objects are + immutable. + </description> + </event> + + <request name="get_image_description"> + <description summary="get the image description of the output"> + This creates a new wp_image_description_v1 object for the current image + description of the output. There always is exactly one image description + active for an output so the client should destroy the image description + created by earlier invocations of this request. This request is usually + sent as a reaction to the image_description_changed event or when + creating a wp_color_management_output_v1 object. + + The image description of an output represents the color encoding the + output expects. There might be performance and power advantages, as well + as improved color reproduction, if a content update matches the image + description of the output it is being shown on. If a content update is + shown on any other output than the one it matches the image description + of, then the color reproduction on those outputs might be considerably + worse. + + The created wp_image_description_v1 object preserves the image + description of the output from the time the object was created. + + The resulting image description object allows get_information request. + + If this protocol object is inert, the resulting image description object + shall immediately deliver the wp_image_description_v1.failed event with + the no_output cause. + + If the interface version is inadequate for the output's image + description, meaning that the client does not support all the events + needed to deliver the crucial information, the resulting image + description object shall immediately deliver the + wp_image_description_v1.failed event with the low_version cause. + + Otherwise the object shall immediately deliver the ready event. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + </interface> + + <interface name="wp_color_management_surface_v1" version="3"> + <description summary="color management extension to a surface"> + A wp_color_management_surface_v1 allows the client to set the color + space and HDR properties of a surface. + + If the wl_surface associated with the wp_color_management_surface_v1 is + destroyed, the wp_color_management_surface_v1 object becomes inert. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the color management interface for a surface"> + Destroy the wp_color_management_surface_v1 object and do the same as + unset_image_description. + </description> + </request> + + <enum name="error"> + <description summary="protocol errors"/> + <entry name="render_intent" value="0" + summary="unsupported rendering intent"/> + <entry name="image_description" value="1" + summary="invalid image description"/> + <entry name="inert" value="2" + summary="forbidden request on inert object"/> + </enum> + + <request name="set_image_description"> + <description summary="set the surface image description"> + If this protocol object is inert, the protocol error inert is raised. + + Set the image description of the underlying surface. The image + description and rendering intent are double-buffered state, see + wl_surface.commit. + + It is the client's responsibility to understand the image description + it sets on a surface, and to provide content that matches that image + description. Compositors might convert images to match their own or any + other image descriptions. + + Image descriptions which are not ready (see wp_image_description_v1) + are forbidden in this request, and in such case the protocol error + image_description is raised. + + All image descriptions which are ready (see wp_image_description_v1) + are allowed and must always be accepted by the compositor. + + When an image description is set on a surface, it establishes an + explicit link between surface pixel values and surface colorimetry. + This link may be undefined for some pixel values, see the image + description creator interfaces for the conditions. Non-finite + floating-point values (NaN, Inf) always have an undefined colorimetry. + + A rendering intent provides the client's preference on how surface + colorimetry should be mapped to each output. The render_intent value + must be one advertised by the compositor with + wp_color_manager_v1.render_intent event, otherwise the protocol error + render_intent is raised. + + By default, a surface does not have an associated image description + nor a rendering intent. The handling of color on such surfaces is + compositor implementation defined. Compositors should handle such + surfaces as sRGB, but may handle them differently if they have specific + requirements. + + Setting the image description has copy semantics; after this request, + the image description can be immediately destroyed without affecting + the pending state of the surface. + </description> + + <arg name="image_description" + type="object" interface="wp_image_description_v1"/> + <arg name="render_intent" + type="uint" enum="wp_color_manager_v1.render_intent" + summary="rendering intent"/> + </request> + + <request name="unset_image_description"> + <description summary="remove the surface image description"> + If this protocol object is inert, the protocol error inert is raised. + + This request removes any image description from the surface. See + set_image_description for how a compositor handles a surface without + an image description. This is double-buffered state, see + wl_surface.commit. + </description> + </request> + </interface> + + <interface name="wp_color_management_surface_feedback_v1" version="3"> + <description summary="color management extension to a surface"> + A wp_color_management_surface_feedback_v1 allows the client to get the + preferred image description of a surface. + + If the wl_surface associated with this object is destroyed, the + wp_color_management_surface_feedback_v1 object becomes inert. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the color management interface for a surface"> + Destroy the wp_color_management_surface_feedback_v1 object. + </description> + </request> + + <enum name="error"> + <description summary="protocol errors"/> + <entry name="inert" value="0" + summary="forbidden request on inert object"/> + <entry name="unsupported_feature" value="1" + summary="attempted to use an unsupported feature"/> + </enum> + + <event name="preferred_changed" deprecated-since="2"> + <description summary="the preferred image description changed (32-bit)"> + Starting from interface version 2, 'preferred_changed2' is sent instead + of this event. See the 'preferred_changed2' event for the definition. + </description> + + <arg name="identity" type="uint" + summary="the 32-bit image description id number"/> + </event> + + <request name="get_preferred"> + <description summary="get the preferred image description"> + If this protocol object is inert, the protocol error inert is raised. + + The preferred image description represents the compositor's preferred + color encoding for this wl_surface at the current time. There might be + performance and power advantages, as well as improved color + reproduction, if the image description of a content update matches the + preferred image description. + + This creates a new wp_image_description_v1 object for the currently + preferred image description for the wl_surface. The client should + stop using and destroy the image descriptions created by earlier + invocations of this request for the associated wl_surface. + This request is usually sent as a reaction to the preferred_changed + event or when creating a wp_color_management_surface_feedback_v1 object + if the client is capable of adapting to image descriptions. + + The created wp_image_description_v1 object preserves the preferred image + description of the wl_surface from the time the object was created. + + The resulting image description object allows get_information request. + + If the image description is parametric, the client should set it on its + wl_surface only if the image description is an exact match with the + client content. Particularly if everything else matches, but the target + color volume is greater than what the client needs, the client should + create its own parameric image description with its exact parameters. + + If the interface version is inadequate for the preferred image + description, meaning that the client does not support all the + events needed to deliver the crucial information, the resulting image + description object shall immediately deliver the + wp_image_description_v1.failed event with the low_version cause, + otherwise the object shall immediately deliver the ready event. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + + <request name="get_preferred_parametric"> + <description summary="get the preferred image description"> + The same description as for get_preferred applies, except the returned + image description is guaranteed to be parametric. This is meant for + clients that can only deal with parametric image descriptions. + + If the compositor doesn't support parametric image descriptions, the + unsupported_feature error is emitted. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + + <!-- Version 2 additions --> + + <event name="preferred_changed2" since="2"> + <description summary="the preferred image description changed"> + The preferred image description is the one which likely has the most + performance and/or quality benefits for the compositor if used by the + client for its wl_surface contents. This event is sent whenever the + compositor changes the wl_surface's preferred image description. + + This event sends the identity of the new preferred state as the argument, + so clients who are aware of the image description already can reuse it. + Otherwise, if the client client wants to know what the preferred image + description is, it shall use the get_preferred request. + + The preferred image description is not automatically used for anything. + It is only a hint, and clients may set any valid image description with + set_image_description, but there might be performance and color accuracy + improvements by providing the wl_surface contents in the preferred + image description. Therefore clients that can, should render according + to the preferred image description + </description> + + <arg name="identity_hi" type="uint" + summary="high 32 bits of the 64-bit image description id number"/> + <arg name="identity_lo" type="uint" + summary="low 32 bits of the 64-bit image description id number"/> + </event> + + </interface> + + <interface name="wp_image_description_creator_icc_v1" version="3"> + <description summary="holder of image description ICC information"> + This type of object is used for collecting all the information required + to create a wp_image_description_v1 object from an ICC file. A complete + set of required parameters consists of these properties: + - ICC file + + Each required property must be set exactly once if the client is to create + an image description. The set requests verify that a property was not + already set. The create request verifies that all required properties are + set. There may be several alternative requests for setting each property, + and in that case the client must choose one of them. + + Once all properties have been set, the create request must be used to + create the image description object, destroying the creator in the + process. + + The link between a pixel value (a device value in ICC) and its respective + colorimetry is defined by the details of the particular ICC profile. + Those details also determine when colorimetry becomes undefined. + </description> + + <enum name="error"> + <description summary="protocol errors"/> + + <entry name="incomplete_set" value="0" + summary="incomplete parameter set"/> + <entry name="already_set" value="1" + summary="property already set"/> + <entry name="bad_fd" value="2" + summary="fd not seekable and readable"/> + <entry name="bad_size" value="3" + summary="no or too much data"/> + <entry name="out_of_file" value="4" + summary="offset + length exceeds file size"/> + </enum> + + <request name="create" type="destructor"> + <description summary="Create the image description object from ICC data"> + Create an image description object based on the ICC information + previously set on this object. A compositor must parse the ICC data in + some undefined but finite amount of time. + + The completeness of the parameter set is verified. If the set is not + complete, the protocol error incomplete_set is raised. For the + definition of a complete set, see the description of this interface. + + If the particular combination of the information is not supported + by the compositor, the resulting image description object shall + immediately deliver the wp_image_description_v1.failed event with the + 'unsupported' cause. If a valid image description was created from the + information, the wp_image_description_v1.ready event will eventually + be sent instead. + + This request destroys the wp_image_description_creator_icc_v1 object. + + The resulting image description object does not allow get_information + request. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + + <request name="set_icc_file"> + <description summary="set the ICC profile file"> + Sets the ICC profile file to be used as the basis of the image + description. + + The data shall be found through the given fd at the given offset, having + the given length. The fd must be seekable and readable. Violating these + requirements raises the bad_fd protocol error. + + If reading the data fails due to an error independent of the client, the + compositor shall send the wp_image_description_v1.failed event on the + created wp_image_description_v1 with the 'operating_system' cause. + + The maximum size of the ICC profile is 32 MB. If length is greater than + that or zero, the protocol error bad_size is raised. If offset + length + exceeds the file size, the protocol error out_of_file is raised. + + A compositor may read the file at any time starting from this request + and only until whichever happens first: + - If create request was issued, the wp_image_description_v1 object + delivers either failed or ready event; or + - if create request was not issued, this + wp_image_description_creator_icc_v1 object is destroyed. + + A compositor shall not modify the contents of the file, and the fd may + be sealed for writes and size changes. The client must ensure to its + best ability that the data does not change while the compositor is + reading it. + + The data must represent a valid ICC profile. The ICC profile version + must be 2 or 4, it must be a 3 channel profile and the class must be + Display or ColorSpace. Violating these requirements will not result in a + protocol error, but will eventually send the + wp_image_description_v1.failed event on the created + wp_image_description_v1 with the 'unsupported' cause. + + See the International Color Consortium specification ICC.1:2022 for more + details about ICC profiles. + + If ICC file has already been set on this object, the protocol error + already_set is raised. + </description> + + <arg name="icc_profile" type="fd" + summary="ICC profile"/> + <arg name="offset" type="uint" + summary="byte offset in fd to start of ICC data"/> + <arg name="length" type="uint" + summary="length of ICC data in bytes"/> + </request> + </interface> + + <interface name="wp_image_description_creator_params_v1" version="3"> + <description summary="holder of image description parameters"> + This type of object is used for collecting all the parameters required + to create a wp_image_description_v1 object. A complete set of required + parameters consists of these properties: + - transfer characteristic function (tf) + - chromaticities of primaries and white point (primary color volume) + + The following properties are optional and have a well-defined default + if not explicitly set: + - primary color volume luminance range + - reference white luminance level + - mastering display primaries and white point (target color volume) + - mastering luminance range + + The following properties are optional and will be ignored + if not explicitly set: + - maximum content light level + - maximum frame-average light level + + Each required property must be set exactly once if the client is to create + an image description. The set requests verify that a property was not + already set. The create request verifies that all required properties are + set. There may be several alternative requests for setting each property, + and in that case the client must choose one of them. + + Once all properties have been set, the create request must be used to + create the image description object, destroying the creator in the + process. + + A viewer, who is viewing the display defined by the resulting image + description (the viewing environment included), is assumed to be fully + adapted to the primary color volume's white point. + + Any of the following conditions will cause the colorimetry of a pixel + to become undefined: + - Values outside of the defined range of the transfer characteristic. + - Tristimulus that exceeds the target color volume. + - If extended_target_volume is not supported: tristimulus that exceeds + the primary color volume. + + The closest correspondence to an image description created through this + interface is the Display class of profiles in ICC. + </description> + + <enum name="error"> + <description summary="protocol errors"/> + + <entry name="incomplete_set" value="0" + summary="incomplete parameter set"/> + <entry name="already_set" value="1" + summary="property already set"/> + <entry name="unsupported_feature" value="2" + summary="request not supported"/> + <entry name="invalid_tf" value="3" + summary="invalid transfer characteristic"/> + <entry name="invalid_primaries_named" value="4" + summary="invalid primaries named"/> + <entry name="invalid_luminance" value="5" + summary="invalid luminance value or range"/> + </enum> + + <request name="create" type="destructor"> + <description summary="Create the image description object using params"> + Create an image description object based on the parameters previously + set on this object. + + The completeness of the parameter set is verified. If the set is not + complete, the protocol error incomplete_set is raised. For the + definition of a complete set, see the description of this interface. + + When both max_cll and max_fall are set, max_fall must be less or equal + to max_cll otherwise the invalid_luminance protocol error is raised. + + In version 1, these following conditions also result in the + invalid_luminance protocol error. Version 2 and later do not have this + requirement. + - When max_cll is set, it must be greater than min L and less or equal + to max L of the mastering luminance range. + - When max_fall is set, it must be greater than min L and less or equal + to max L of the mastering luminance range. + + If the particular combination of the parameter set is not supported + by the compositor, the resulting image description object shall + immediately deliver the wp_image_description_v1.failed event with the + 'unsupported' cause. If a valid image description was created from the + parameter set, the wp_image_description_v1.ready event will eventually + be sent instead. + + This request destroys the wp_image_description_creator_params_v1 + object. + + The resulting image description object does not allow get_information + request. + </description> + + <arg name="image_description" + type="new_id" interface="wp_image_description_v1"/> + </request> + + <request name="set_tf_named"> + <description summary="named transfer characteristic"> + Sets the transfer characteristic using explicitly enumerated named + functions. + + When the resulting image description is attached to an image, the + content should be decoded according to the industry standard + practices for the transfer characteristic. + + Only names advertised with wp_color_manager_v1 event supported_tf_named + are allowed. Other values shall raise the protocol error invalid_tf. + + If transfer characteristic has already been set on this object, the + protocol error already_set is raised. + </description> + + <arg name="tf" type="uint" enum="wp_color_manager_v1.transfer_function" + summary="named transfer function"/> + </request> + + <request name="set_tf_power"> + <description summary="transfer characteristic as a power curve"> + Sets the color component transfer characteristic to a power curve with + the given exponent. Negative values are handled by mirroring the + positive half of the curve through the origin. The valid domain and + range of the curve are all finite real numbers. This curve represents + the conversion from electrical to optical color channel values. + + The curve exponent shall be multiplied by 10000 to get the argument eexp + value to carry the precision of 4 decimals. + + The curve exponent must be at least 1.0 and at most 10.0. Otherwise the + protocol error invalid_tf is raised. + + If transfer characteristic has already been set on this object, the + protocol error already_set is raised. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.set_tf_power. Otherwise this request raises + the protocol error unsupported_feature. + </description> + + <arg name="eexp" type="uint" summary="the exponent * 10000"/> + </request> + + <request name="set_primaries_named"> + <description summary="named primaries"> + Sets the color primaries and white point using explicitly named sets. + This describes the primary color volume which is the basis for color + value encoding. + + Only names advertised with wp_color_manager_v1 event + supported_primaries_named are allowed. Other values shall raise the + protocol error invalid_primaries_named. + + If primaries have already been set on this object, the protocol error + already_set is raised. + </description> + + <arg name="primaries" type="uint" enum="wp_color_manager_v1.primaries" + summary="named primaries"/> + </request> + + <request name="set_primaries"> + <description summary="primaries as chromaticity coordinates"> + Sets the color primaries and white point using CIE 1931 xy chromaticity + coordinates. This describes the primary color volume which is the basis + for color value encoding. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + If primaries have already been set on this object, the protocol error + already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_primaries. Otherwise this request raises + the protocol error unsupported_feature. + </description> + + <arg name="r_x" type="int" summary="Red x * 1M"/> + <arg name="r_y" type="int" summary="Red y * 1M"/> + <arg name="g_x" type="int" summary="Green x * 1M"/> + <arg name="g_y" type="int" summary="Green y * 1M"/> + <arg name="b_x" type="int" summary="Blue x * 1M"/> + <arg name="b_y" type="int" summary="Blue y * 1M"/> + <arg name="w_x" type="int" summary="White x * 1M"/> + <arg name="w_y" type="int" summary="White y * 1M"/> + </request> + + <request name="set_luminances"> + <description summary="primary color volume luminance range and reference white"> + Sets the primary color volume luminance range and the reference white + luminance level. These values include the minimum display emission, but + not external flare. The minimum display emission is assumed to have + the chromaticity of the primary color volume white point. + + The default luminances from + https://www.color.org/chardata/rgb/srgb.xalter are + - primary color volume minimum: 0.2 cd/m² + - primary color volume maximum: 80 cd/m² + - reference white: 80 cd/m² + + Setting a named transfer characteristic can imply other default + luminances. + + The default luminances get overwritten when this request is used. + With transfer_function.st2084_pq the given 'max_lum' value is ignored, + and 'max_lum' is taken as 'min_lum' + 10000 cd/m². + + 'min_lum' and 'max_lum' specify the minimum and maximum luminances of + the primary color volume as reproduced by the targeted display. + + 'reference_lum' specifies the luminance of the reference white as + reproduced by the targeted display, and reflects the targeted viewing + environment. + + Compositors should make sure that all content is anchored, meaning that + an input signal level of 'reference_lum' on one image description and + another input signal level of 'reference_lum' on another image + description should produce the same output level, even though the + 'reference_lum' on both image representations can be different. + + 'reference_lum' may be higher than 'max_lum'. In that case reaching + the reference white output level in image content requires the + 'extended_target_volume' feature support. + + If 'max_lum' or 'reference_lum' are less than or equal to 'min_lum', + the protocol error invalid_luminance is raised. + + The minimum luminance is multiplied by 10000 to get the argument + 'min_lum' value and carries precision of 4 decimals. The maximum + luminance and reference white luminance values are unscaled. + + If the primary color volume luminance range and the reference white + luminance level have already been set on this object, the protocol error + already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_luminances. Otherwise this request + raises the protocol error unsupported_feature. + </description> + + <arg name="min_lum" type="uint" + summary="minimum luminance (cd/m²) * 10000"/> + <arg name="max_lum" type="uint" + summary="maximum luminance (cd/m²)"/> + <arg name="reference_lum" type="uint" + summary="reference white luminance (cd/m²)"/> + </request> + + <request name="set_mastering_display_primaries"> + <description summary="mastering display primaries as chromaticity coordinates"> + Provides the color primaries and white point of the mastering display + using CIE 1931 xy chromaticity coordinates. This is compatible with the + SMPTE ST 2086 definition of HDR static metadata. + + The mastering display primaries and mastering display luminances define + the target color volume. + + If mastering display primaries are not explicitly set, the target color + volume is assumed to have the same primaries as the primary color volume. + + The target color volume is defined by all tristimulus values between 0.0 + and 1.0 (inclusive) of the color space defined by the given mastering + display primaries and white point. The colorimetry is identical between + the container color space and the mastering display color space, + including that no chromatic adaptation is applied even if the white + points differ. + + The target color volume can exceed the primary color volume to allow for + a greater color volume with an existing color space definition (for + example scRGB). It can be smaller than the primary color volume to + minimize gamut and tone mapping distances for big color spaces (HDR + metadata). + + To make use of the entire target color volume a suitable pixel format + has to be chosen (e.g. floating point to exceed the primary color + volume, or abusing limited quantization range as with xvYCC). + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + If mastering display primaries have already been set on this object, the + protocol error already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise + this request raises the protocol error unsupported_feature. The + advertisement implies support only for target color volumes fully + contained within the primary color volume. + + If a compositor additionally supports target color volume exceeding the + primary color volume, it must advertise + wp_color_manager_v1.feature.extended_target_volume. If a client uses + target color volume exceeding the primary color volume and the + compositor does not support it, the result is implementation defined. + Compositors are recommended to detect this case and fail the image + description gracefully, but it may as well result in color artifacts. + </description> + + <arg name="r_x" type="int" summary="Red x * 1M"/> + <arg name="r_y" type="int" summary="Red y * 1M"/> + <arg name="g_x" type="int" summary="Green x * 1M"/> + <arg name="g_y" type="int" summary="Green y * 1M"/> + <arg name="b_x" type="int" summary="Blue x * 1M"/> + <arg name="b_y" type="int" summary="Blue y * 1M"/> + <arg name="w_x" type="int" summary="White x * 1M"/> + <arg name="w_y" type="int" summary="White y * 1M"/> + </request> + + <request name="set_mastering_luminance"> + <description summary="display mastering luminance range"> + Sets the luminance range that was used during the content mastering + process as the minimum and maximum absolute luminance L. These values + include the minimum display emission and ambient flare luminances, + assumed to be optically additive and have the chromaticity of the + primary color volume white point. This should be + compatible with the SMPTE ST 2086 definition of HDR static metadata. + + The mastering display primaries and mastering display luminances define + the target color volume. + + If mastering luminances are not explicitly set, the target color volume + is assumed to have the same min and max luminances as the primary color + volume. + + If max L is less than or equal to min L, the protocol error + invalid_luminance is raised. + + Min L value is multiplied by 10000 to get the argument min_lum value + and carry precision of 4 decimals. Max L value is unscaled for max_lum. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise + this request raises the protocol error unsupported_feature. The + advertisement implies support only for target color volumes fully + contained within the primary color volume. + + If a compositor additionally supports target color volume exceeding the + primary color volume, it must advertise + wp_color_manager_v1.feature.extended_target_volume. If a client uses + target color volume exceeding the primary color volume and the + compositor does not support it, the result is implementation defined. + Compositors are recommended to detect this case and fail the image + description gracefully, but it may as well result in color artifacts. + </description> + + <arg name="min_lum" type="uint" summary="min L (cd/m²) * 10000"/> + <arg name="max_lum" type="uint" summary="max L (cd/m²)"/> + </request> + + <request name="set_max_cll"> + <description summary="maximum content light level"> + Sets the maximum content light level (max_cll) as defined by CTA-861-H. + + max_cll is undefined by default. + </description> + + <arg name="max_cll" type="uint" summary="Maximum content light level (cd/m²)"/> + </request> + + <request name="set_max_fall"> + <description summary="maximum frame-average light level"> + Sets the maximum frame-average light level (max_fall) as defined by + CTA-861-H. + + max_fall is undefined by default. + </description> + + <arg name="max_fall" type="uint" summary="Maximum frame-average light level (cd/m²)"/> + </request> + </interface> + + <interface name="wp_image_description_v1" version="3"> + <description summary="Colorimetric image description"> + An image description carries information about the pixel color encoding + and its intended display and viewing environment. The image description is + attached to a wl_surface via + wp_color_management_surface_v1.set_image_description. A compositor can use + this information to decode pixel values into colorimetrically meaningful + quantities, which allows the compositor to transform the surface contents + to become suitable for various displays and viewing environments. + + Note, that the wp_image_description_v1 object is not ready to be used + immediately after creation. The object eventually delivers either the + 'ready' or the 'failed' event, specified in all requests creating it. The + object is deemed "ready" after receiving the 'ready' event. + + An object which is not ready is illegal to use, it can only be destroyed. + Any other request in this interface shall result in the 'not_ready' + protocol error. Attempts to use an object which is not ready through other + interfaces shall raise protocol errors defined there. + + Once created and regardless of how it was created, a + wp_image_description_v1 object always refers to one fixed image + description. It cannot change after creation. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the image description"> + Destroy this object. It is safe to destroy an object which is not ready. + + Destroying a wp_image_description_v1 object has no side-effects, not + even if a wp_color_management_surface_v1.set_image_description has not + yet been followed by a wl_surface.commit. + </description> + </request> + + <enum name="error"> + <description summary="protocol errors"/> + + <entry name="not_ready" value="0" + summary="attempted to use an object which is not ready"/> + <entry name="no_information" value="1" + summary="get_information not allowed"/> + </enum> + + <enum name="cause"> + <description summary="generic reason for failure"/> + + <entry name="low_version" value="0" + summary="interface version too low"/> + <entry name="unsupported" value="1" + summary="unsupported image description data"/> + <entry name="operating_system" value="2" + summary="error independent of the client"/> + <entry name="no_output" value="3" + summary="the relevant output no longer exists"/> + </enum> + + <event name="failed"> + <description summary="graceful error on creating the image description"> + If creating a wp_image_description_v1 object fails for a reason that is + not defined as a protocol error, this event is sent. + + The requests that create image description objects define whether and + when this can occur. Only such creation requests can trigger this event. + This event cannot be triggered after the image description was + successfully formed. + + Once this event has been sent, the wp_image_description_v1 object will + never become ready and it can only be destroyed. + </description> + + <arg name="cause" type="uint" enum="cause" + summary="generic reason"/> + <arg name="msg" type="string" + summary="ad hoc human-readable explanation"/> + </event> + + <event name="ready" deprecated-since="2"> + <description summary="the object is ready to be used (32-bit)"> + Starting from interface version 2, the 'ready2' event is sent instead + of this event. + + For the definition of this event, see the 'ready2' event. The + difference to this event is as follows. + + The id number is valid only as long as the protocol object is alive. If + all protocol objects referring to the same image description record are + destroyed, the id number may be recycled for a different image + description record. + </description> + + <arg name="identity" type="uint" + summary="the 32-bit image description id number"/> + </event> + + <request name="get_information"> + <description summary="get information about the image description"> + Creates a wp_image_description_info_v1 object which delivers the + information that makes up the image description. + + Not all image description protocol objects allow get_information + request. Whether it is allowed or not is defined by the request that + created the object. If get_information is not allowed, the protocol + error no_information is raised. + </description> + + <arg name="information" + type="new_id" interface="wp_image_description_info_v1"/> + </request> + + <!-- Version 2 additions --> + + <event name="ready2" since="2"> + <description summary="the object is ready to be used"> + Once this event has been sent, the wp_image_description_v1 object is + deemed "ready". Ready objects can be used to send requests and can be + used through other interfaces. + + Every ready wp_image_description_v1 protocol object refers to an + underlying image description record in the compositor. Multiple protocol + objects may end up referring to the same record. Clients may identify + these "copies" by comparing their id numbers: if the numbers from two + protocol objects are identical, the protocol objects refer to the same + image description record. Two different image description records + cannot have the same id number simultaneously. The id number does not + change during the lifetime of the image description record. + + Image description id number is not a protocol object id. Zero is + reserved as an invalid id number. It shall not be possible for a client + to refer to an image description by its id number in protocol. The id + numbers might not be portable between Wayland connections. A compositor + shall not send an invalid id number. + + Compositors must not recycle image description id numbers. + + This identity allows clients to de-duplicate image description records + and avoid get_information request if they already have the image + description information. + </description> + + <arg name="identity_hi" type="uint" + summary="high 32 bits of the 64-bit image description id number"/> + <arg name="identity_lo" type="uint" + summary="low 32 bits of the 64-bit image description id number"/> + </event> + </interface> + + <interface name="wp_image_description_info_v1" version="3"> + <description summary="Colorimetric image description information"> + Sends all matching events describing an image description object exactly + once and finally sends the 'done' event. + + This means + - if the image description is parametric, it must send + - primaries + - named_primaries, if applicable + - at least one of tf_power and tf_named, as applicable + - luminances + - target_primaries + - target_luminance + - if the image description is parametric, it may send, if applicable, + - target_max_cll + - target_max_fall + - if the image description contains an ICC profile, it must send the + icc_file event + + Once a wp_image_description_info_v1 object has delivered a 'done' event it + is automatically destroyed. + + Every wp_image_description_info_v1 created from the same + wp_image_description_v1 shall always return the exact same data. + </description> + + <event name="done" type="destructor"> + <description summary="end of information"> + Signals the end of information events and destroys the object. + </description> + </event> + + <event name="icc_file"> + <description summary="ICC profile matching the image description"> + The icc argument provides a file descriptor to the client which may be + memory-mapped to provide the ICC profile matching the image description. + The fd is read-only, and if mapped then it must be mapped with + MAP_PRIVATE by the client. + + The ICC profile version and other details are determined by the + compositor. There is no provision for a client to ask for a specific + kind of a profile. + </description> + + <arg name="icc" type="fd" summary="ICC profile file descriptor"/> + <arg name="icc_size" type="uint" summary="ICC profile size, in bytes"/> + <!-- Offset always 0, compositor must not expose unnecessary data. --> + </event> + + <event name="primaries"> + <description summary="primaries as chromaticity coordinates"> + Delivers the primary color volume primaries and white point using CIE + 1931 xy chromaticity coordinates. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + </description> + + <arg name="r_x" type="int" summary="Red x * 1M"/> + <arg name="r_y" type="int" summary="Red y * 1M"/> + <arg name="g_x" type="int" summary="Green x * 1M"/> + <arg name="g_y" type="int" summary="Green y * 1M"/> + <arg name="b_x" type="int" summary="Blue x * 1M"/> + <arg name="b_y" type="int" summary="Blue y * 1M"/> + <arg name="w_x" type="int" summary="White x * 1M"/> + <arg name="w_y" type="int" summary="White y * 1M"/> + </event> + + <event name="primaries_named"> + <description summary="named primaries"> + Delivers the primary color volume primaries and white point using an + explicitly enumerated named set. + </description> + + <arg name="primaries" type="uint" enum="wp_color_manager_v1.primaries" + summary="named primaries"/> + </event> + + <event name="tf_power"> + <description summary="transfer characteristic as a power curve"> + The color component transfer characteristic of this image description is + a pure power curve. This event provides the exponent of the power + function. This curve represents the conversion from electrical to + optical pixel or color values. + + The curve exponent has been multiplied by 10000 to get the argument eexp + value to carry the precision of 4 decimals. + </description> + + <arg name="eexp" type="uint" summary="the exponent * 10000"/> + </event> + + <event name="tf_named"> + <description summary="named transfer characteristic"> + Delivers the transfer characteristic using an explicitly enumerated + named function. + </description> + + <arg name="tf" type="uint" enum="wp_color_manager_v1.transfer_function" + summary="named transfer function"/> + </event> + + <event name="luminances"> + <description summary="primary color volume luminance range and reference white"> + Delivers the primary color volume luminance range and the reference + white luminance level. These values include the minimum display emission + and ambient flare luminances, assumed to be optically additive and have + the chromaticity of the primary color volume white point. + + The minimum luminance is multiplied by 10000 to get the argument + 'min_lum' value and carries precision of 4 decimals. The maximum + luminance and reference white luminance values are unscaled. + </description> + + <arg name="min_lum" type="uint" + summary="minimum luminance (cd/m²) * 10000"/> + <arg name="max_lum" type="uint" + summary="maximum luminance (cd/m²)"/> + <arg name="reference_lum" type="uint" + summary="reference white luminance (cd/m²)"/> + </event> + + <event name="target_primaries"> + <description summary="target primaries as chromaticity coordinates"> + Provides the color primaries and white point of the target color volume + using CIE 1931 xy chromaticity coordinates. This is compatible with the + SMPTE ST 2086 definition of HDR static metadata for mastering displays. + + While primary color volume is about how color is encoded, the target + color volume is the actually displayable color volume. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + </description> + + <arg name="r_x" type="int" summary="Red x * 1M"/> + <arg name="r_y" type="int" summary="Red y * 1M"/> + <arg name="g_x" type="int" summary="Green x * 1M"/> + <arg name="g_y" type="int" summary="Green y * 1M"/> + <arg name="b_x" type="int" summary="Blue x * 1M"/> + <arg name="b_y" type="int" summary="Blue y * 1M"/> + <arg name="w_x" type="int" summary="White x * 1M"/> + <arg name="w_y" type="int" summary="White y * 1M"/> + </event> + + <event name="target_luminance"> + <description summary="target luminance range"> + Provides the luminance range that the image description is targeting as + the minimum and maximum absolute luminance L. These values include the + minimum display emission and ambient flare luminances, assumed to be + optically additive and have the chromaticity of the primary color + volume white point. This should be compatible with the SMPTE ST 2086 + definition of HDR static metadata. + + This luminance range is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + + Min L value is multiplied by 10000 to get the argument min_lum value and + carry precision of 4 decimals. Max L value is unscaled for max_lum. + </description> + + <arg name="min_lum" type="uint" summary="min L (cd/m²) * 10000"/> + <arg name="max_lum" type="uint" summary="max L (cd/m²)"/> + </event> + + <event name="target_max_cll"> + <description summary="target maximum content light level"> + Provides the targeted max_cll of the image description. max_cll is + defined by CTA-861-H. + + This luminance is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + </description> + + <arg name="max_cll" type="uint" + summary="Maximum content light-level (cd/m²)"/> + </event> + + <event name="target_max_fall"> + <description summary="target maximum frame-average light level"> + Provides the targeted max_fall of the image description. max_fall is + defined by CTA-861-H. + + This luminance is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + </description> + + <arg name="max_fall" type="uint" + summary="Maximum frame-average light level (cd/m²)"/> + </event> + </interface> + + <interface name="wp_image_description_reference_v1" version="1" frozen="true"> + <description summary="Reference to an image description"> + This object is a reference to an image description. This interface is + frozen at version 1 to allow other protocols to create + wp_image_description_v1 objects. + + The wp_color_manager_v1.get_image_description request can be used to + retrieve the underlying image description. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the reference"> + Destroy this object. This has no effect on the referenced image + description. + </description> + </request> + </interface> +</protocol> diff --git a/protocol/meson.build b/protocol/meson.build index bbdccba..296c057 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -16,6 +16,7 @@ wayland_scanner_client = generator( protocols = [ 'xdg-output-unstable-v1.xml', + 'color-management-v1.xml', ] protocol_src = [] diff --git a/src/args_parser.c b/src/args_parser.c index 1957f32..c37712a 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -204,7 +204,7 @@ static void usage_header(void) { "[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] " "[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [-ffmpeg-opts <options>] [--list-capture-options [card_path]] " "[--list-monitors] [--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-write-first-frame-ts yes|no] [-low-power yes|no] " - "[-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name); + "[-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name); fflush(stdout); } @@ -272,6 +272,7 @@ static bool args_parser_set_values(args_parser *self) { self->fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); self->write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false); self->low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false); + self->exclude_metadata = args_get_boolean_by_key(self->args, NUM_ARGS, "-exclude-metadata", false); self->audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); self->audio_bitrate *= 1000LL; @@ -567,6 +568,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand self->args[arg_index++] = (Arg){ .key = "-ffmpeg-audio-opts", .optional = true, .list = false, .type = ARG_TYPE_STRING }; self->args[arg_index++] = (Arg){ .key = "-write-first-frame-ts", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; self->args[arg_index++] = (Arg){ .key = "-low-power", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-exclude-metadata", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; assert(arg_index == NUM_ARGS); for(int i = 1; i < argc; i += 2) { diff --git a/src/capture/kms.c b/src/capture/kms.c index 1544e2c..31d4b17 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> @@ -11,6 +12,7 @@ #include <fcntl.h> #include <xf86drm.h> +#include <xf86drmMode.h> #include <drm_fourcc.h> #include <libavutil/mastering_display_metadata.h> @@ -19,6 +21,8 @@ #define HDMI_STATIC_METADATA_TYPE1 0 #define HDMI_EOTF_SMPTE_ST2084 2 +#define HDR_PEAK_LUMINANCE_FALLBACK 1000.0f +#define SDR_WHITE_LUMINANCE_REFERENCE 203.0f #define MAX_CONNECTOR_IDS 32 @@ -46,6 +50,17 @@ 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 night_light_message_shown; + bool hdr_luminance_message_shown; + float hdr_video_max_luminance; bool is_x11; @@ -80,6 +95,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 +171,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; @@ -182,13 +203,15 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture else self->capture_size = rotate_capture_size_if_rotated(self, monitor.size, self->display_server_monitor_rotation); + vec2i capture_size = self->capture_size; + if(self->params.region_size.x > 0 && self->params.region_size.y > 0) + capture_size = self->params.region_size; + if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) { - self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution); + self->params.output_resolution = scale_keep_aspect_ratio(capture_size, self->params.output_resolution); capture_metadata->video_size = self->params.output_resolution; - } else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) { - capture_metadata->video_size = self->params.region_size; } else { - capture_metadata->video_size = self->capture_size; + capture_metadata->video_size = capture_size; } self->last_time_monitor_check = clock_get_monotonic_seconds(); @@ -258,6 +281,236 @@ 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_get_monitor_luminances(gsr_capture_kms *self, const gsr_kms_response_item *drm_fd, float *sdr_white_luminance, float *hdr_peak_luminance) { + *sdr_white_luminance = 0.0f; + *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 <= 0.0f) + *sdr_white_luminance = SDR_WHITE_LUMINANCE_REFERENCE; + + if(*sdr_white_luminance > *hdr_peak_luminance) + *sdr_white_luminance = *hdr_peak_luminance; +} + +static void gsr_capture_kms_update_hdr_color_transforms(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); + + float night_light_matrix[9]; + const bool night_light = self->params.kde_night_light && gsr_kde_night_light_get_inverse_matrix(self->params.kde_night_light, night_light_matrix); + if(night_light && !self->night_light_message_shown) { + self->night_light_message_shown = true; + fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: night light is active, removing the night light tint from the capture\n"); + } + + float sdr_white_luminance = 0.0f; + float hdr_peak_luminance = 0.0f; + gsr_capture_kms_get_monitor_luminances(self, drm_fd, &sdr_white_luminance, &hdr_peak_luminance); + + /* Hdr passthrough: the compositor composites sdr white at the monitors sdr white luminance while the video should have + sdr white at the reference luminance (203 nits), so the luminance of the video is scaled to match that */ + float luminance_scale = 1.0f; + if(self->params.hdr && plane_is_hdr) { + luminance_scale = SDR_WHITE_LUMINANCE_REFERENCE / sdr_white_luminance; + if(luminance_scale != 1.0f && !self->hdr_luminance_message_shown) { + self->hdr_luminance_message_shown = true; + fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: scaling the luminance of the hdr video from sdr white at %d nits to sdr white at %d nits (hdr peak luminance: %d nits)\n", + (int)sdr_white_luminance, (int)SDR_WHITE_LUMINANCE_REFERENCE, (int)(hdr_peak_luminance * luminance_scale)); + } + } + + const bool convert_sdr_to_hdr = self->params.hdr && !plane_is_hdr; + if(convert_sdr_to_hdr && !self->hdr_luminance_message_shown) { + self->hdr_luminance_message_shown = true; + fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: the monitor is in sdr mode, converting the captured sdr image to hdr (BT.709 to BT.2020 with sdr white at %d nits)\n", + (int)SDR_WHITE_LUMINANCE_REFERENCE); + } + + const bool tone_map_hdr_to_sdr = !self->params.hdr && plane_is_hdr; + gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, tone_map_hdr_to_sdr, hdr_peak_luminance, sdr_white_luminance); + if(tone_map_hdr_to_sdr && !self->tone_mapping_message_shown) { + self->tone_mapping_message_shown = true; + fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: 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, (int)hdr_peak_luminance); + } + + if(convert_sdr_to_hdr) { + /* ITU-R BT.2087 */ + static const float BT709_TO_BT2020[9] = { + 0.627404f, 0.329283f, 0.043313f, + 0.069097f, 0.919540f, 0.011362f, + 0.016391f, 0.088013f, 0.895595f + }; + float color_matrix[9]; + for(int i = 0; i < 9; ++i) { + color_matrix[i] = BT709_TO_BT2020[i] * (SDR_WHITE_LUMINANCE_REFERENCE / 10000.0f); + if(night_light) + color_matrix[i] *= night_light_matrix[(i % 3) * 4]; + } + gsr_color_conversion_set_color_matrix(color_conversion, color_matrix, GSR_COLOR_MATRIX_TRANSFER_GAMMA22_TO_PQ); + } else if(night_light || luminance_scale != 1.0f) { + float color_matrix[9] = { + luminance_scale, 0.0f, 0.0f, + 0.0f, luminance_scale, 0.0f, + 0.0f, 0.0f, luminance_scale + }; + if(night_light) { + for(int i = 0; i < 9; ++i) { + color_matrix[i] = night_light_matrix[i] * luminance_scale; + } + } + gsr_color_conversion_set_color_matrix(color_conversion, color_matrix, plane_is_hdr ? GSR_COLOR_MATRIX_TRANSFER_PQ : GSR_COLOR_MATRIX_TRANSFER_GAMMA22); + } else { + gsr_color_conversion_set_color_matrix(color_conversion, NULL, GSR_COLOR_MATRIX_TRANSFER_PQ); + } +} + +static void gsr_kms_set_sdr_in_hdr_metadata(gsr_capture_kms *self) { + if(self->hdr_metadata_set) + return; + + self->hdr_metadata_set = true; + memset(&self->hdr_metadata, 0, sizeof(self->hdr_metadata)); + self->hdr_metadata.metadata_type = HDMI_STATIC_METADATA_TYPE1; + self->hdr_metadata.hdmi_metadata_type1.metadata_type = HDMI_STATIC_METADATA_TYPE1; + self->hdr_metadata.hdmi_metadata_type1.eotf = HDMI_EOTF_SMPTE_ST2084; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[0].x = 0.64 * 50000; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[0].y = 0.33 * 50000; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[1].x = 0.30 * 50000; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[1].y = 0.60 * 50000; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[2].x = 0.15 * 50000; + self->hdr_metadata.hdmi_metadata_type1.display_primaries[2].y = 0.06 * 50000; + self->hdr_metadata.hdmi_metadata_type1.white_point.x = 0.3127 * 50000; + self->hdr_metadata.hdmi_metadata_type1.white_point.y = 0.3290 * 50000; + self->hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance = SDR_WHITE_LUMINANCE_REFERENCE; + self->hdr_metadata.hdmi_metadata_type1.max_cll = SDR_WHITE_LUMINANCE_REFERENCE; + self->hdr_video_max_luminance = SDR_WHITE_LUMINANCE_REFERENCE; +} + // 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) @@ -265,6 +518,11 @@ static void gsr_kms_set_hdr_metadata(gsr_capture_kms *self, const gsr_kms_respon self->hdr_metadata_set = true; self->hdr_metadata = drm_fd->hdr_metadata; + + float sdr_white_luminance = 0.0f; + float hdr_peak_luminance = 0.0f; + gsr_capture_kms_get_monitor_luminances(self, drm_fd, &sdr_white_luminance, &hdr_peak_luminance); + self->hdr_video_max_luminance = hdr_peak_luminance * (SDR_WHITE_LUMINANCE_REFERENCE / sdr_white_luminance); } static vec2i swap_vec2i(vec2i value) { @@ -465,6 +723,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_color_transforms(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); @@ -519,6 +779,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 = { @@ -569,8 +831,15 @@ static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata * if(!self->drm_fd) return; - 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); + if(drm_plane_is_hdr(self->drm_fd)) + gsr_capture_kms_update_gamma_lut(self, color_conversion); + + if(self->params.hdr) { + if(drm_plane_is_hdr(self->drm_fd)) + gsr_kms_set_hdr_metadata(self, self->drm_fd); + else + gsr_kms_set_sdr_in_hdr_metadata(self); + } const gsr_monitor_rotation plane_rotation = kms_rotation_to_gsr_monitor_rotation(self->drm_fd->rotation); self->final_monitor_rotation = self->capture_is_combined_plane ? GSR_MONITOR_ROT_0 : sub_rotations(self->display_server_monitor_rotation, plane_rotation); @@ -598,6 +867,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_color_transforms(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 }, @@ -656,6 +927,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_color_transforms(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); @@ -723,6 +996,10 @@ 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_color_matrix(color_conversion, NULL, GSR_COLOR_MATRIX_TRANSFER_PQ); + //self->params.egl->glFlush(); //self->params.egl->glFinish(); @@ -761,6 +1038,11 @@ static bool gsr_capture_kms_set_hdr_metadata(gsr_capture *cap, AVMasteringDispla mastering_display_metadata->min_luminance = av_make_q(self->hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance, 10000); mastering_display_metadata->max_luminance = av_make_q(self->hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance, 1); + if(self->hdr_video_max_luminance > 0.0f) { + light_metadata->MaxCLL = (unsigned int)(self->hdr_video_max_luminance + 0.5f); + mastering_display_metadata->max_luminance = av_make_q((int)(self->hdr_video_max_luminance + 0.5f), 1); + } + mastering_display_metadata->has_primaries = true; mastering_display_metadata->has_luminance = true; diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c index 144f88a..1504323 100644 --- a/src/capture/nvfbc.c +++ b/src/capture/nvfbc.c @@ -287,11 +287,12 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu capture_metadata->video_size.x = self->tracking_width; capture_metadata->video_size.y = self->tracking_height; + if(self->params.region_size.x > 0 && self->params.region_size.y > 0) + capture_metadata->video_size = self->params.region_size; + if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) { self->params.output_resolution = scale_keep_aspect_ratio(capture_metadata->video_size, self->params.output_resolution); capture_metadata->video_size = self->params.output_resolution; - } else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) { - capture_metadata->video_size = self->params.region_size; } return 0; diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c index 56b2cd5..c1bef4f 100644 --- a/src/capture/xcomposite.c +++ b/src/capture/xcomposite.c @@ -98,8 +98,11 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) { capture_metadata->video_size = self->texture_size; - } else { + } else if(self->params.follow_focused) { + /* The video size has to stay the same even when the focused window changes, so it cant be based on the size of the captured window */ capture_metadata->video_size = self->params.output_resolution; + } else { + capture_metadata->video_size = scale_keep_aspect_ratio(self->texture_size, self->params.output_resolution); } self->window_resize_timer = clock_get_monotonic_seconds(); diff --git a/src/color_conversion.c b/src/color_conversion.c index c00bfb5..8076c2b 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,99 @@ " 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 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" \ + " 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 COLOR_MATRIX_GLSL \ + "uniform int color_matrix_transfer; \n" \ + "uniform mat3 color_matrix; \n" \ + "vec3 apply_color_matrix(vec3 color) { \n" \ + " if(color_matrix_transfer == 1) { \n" \ + " vec3 luminance = max(color_matrix * pq_to_luminance3(color), vec3(0.0)); \n" \ + " return luminance3_to_pq(luminance); \n" \ + " } else if(color_matrix_transfer == 2) { \n" \ + " vec3 linear_color = pow(max(color, vec3(0.0)), vec3(2.2)); \n" \ + " linear_color = clamp(color_matrix * linear_color, vec3(0.0), vec3(1.0)); \n" \ + " return pow(linear_color, vec3(1.0/2.2)); \n" \ + " } else { \n" \ + " vec3 linear_color = pow(max(color, vec3(0.0)), vec3(2.2)); \n" \ + " return luminance3_to_pq(max(color_matrix * linear_color, vec3(0.0))); \n" \ + " } \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(color_matrix_transfer != 0) \n" \ + " pixel.rgb = apply_color_matrix(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 +173,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 +192,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 +210,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } else { snprintf(fragment_shader, sizeof(fragment_shader), "#version 300 es \n" @@ -121,10 +223,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) @@ -134,10 +237,20 @@ 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"); + uniforms->color_matrix_transfer = egl->glGetUniformLocation(shader->program_id, "color_matrix_transfer"); + uniforms->color_matrix = egl->glGetUniformLocation(shader->program_id, "color_matrix"); + + 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 +268,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 +286,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } else { snprintf(fragment_shader, sizeof(fragment_shader), "#version 300 es \n" @@ -183,10 +299,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) @@ -196,10 +313,20 @@ 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"); + uniforms->color_matrix_transfer = egl->glGetUniformLocation(shader->program_id, "color_matrix_transfer"); + uniforms->color_matrix = egl->glGetUniformLocation(shader->program_id, "color_matrix"); + + 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 +342,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 +358,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } else { snprintf(fragment_shader, sizeof(fragment_shader), "#version 300 es \n" @@ -240,10 +370,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 COLOR_MATRIX_GLSL GAMMA_LUT_GLSL : "", main_code); } if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) @@ -253,6 +384,16 @@ 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"); + uniforms->color_matrix_transfer = egl->glGetUniformLocation(shader->program_id, "color_matrix_transfer"); + uniforms->color_matrix = egl->glGetUniformLocation(shader->program_id, "color_matrix"); + + egl->glUseProgram(shader->program_id); + egl->glUniform1i(egl->glGetUniformLocation(shader->program_id, "gamma_lut"), 1); + egl->glUseProgram(0); return 0; } @@ -311,6 +452,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 +527,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 +608,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) || self->color_matrix_transfer != 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 +730,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 +752,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 +771,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 +793,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 +858,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 +952,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 +1020,43 @@ 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->glUniform1i(self->graphics_uniforms[shader_index].color_matrix_transfer, self->color_matrix_transfer); + self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].color_matrix, 1, GL_TRUE, self->color_matrix); + } 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->glUniform1i(self->graphics_uniforms[shader_index].color_matrix_transfer, self->color_matrix_transfer); + self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].color_matrix, 1, GL_TRUE, self->color_matrix); + } self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); } break; @@ -814,10 +1065,22 @@ 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->glUniform1i(self->graphics_uniforms[shader_index].color_matrix_transfer, self->color_matrix_transfer); + self->params.egl->glUniformMatrix3fv(self->graphics_uniforms[shader_index].color_matrix, 1, GL_TRUE, self->color_matrix); + } self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); break; } @@ -868,6 +1131,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 +1205,71 @@ 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_set_color_matrix(gsr_color_conversion *self, const float *linear_rgb_matrix, gsr_color_matrix_transfer transfer) { + if(!linear_rgb_matrix) { + self->color_matrix_transfer = 0; + return; + } + + memcpy(self->color_matrix, linear_rgb_matrix, sizeof(self->color_matrix)); + self->color_matrix_transfer = 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) { assert(destination_texture_index >= 0 && destination_texture_index < self->params.num_destination_textures); self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[destination_texture_index]); @@ -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/image_writer.c b/src/image_writer.c index afa9744..0b3028d 100644 --- a/src/image_writer.c +++ b/src/image_writer.c @@ -9,6 +9,60 @@ #include <stdint.h> #include <stdio.h> #include <assert.h> +#include <dlfcn.h> + +#define TJPF_RGBA 7 +#define TJSAMP_420 2 + +typedef void* tjhandle; +typedef tjhandle (*FUNC_tjInitCompress)(void); +typedef int (*FUNC_tjCompress2)(tjhandle handle, const unsigned char *srcBuf, + int width, int pitch, int height, int pixelFormat, + unsigned char **jpegBuf, unsigned long *jpegSize, + int jpegSubsamp, int jpegQual, int flags); +typedef int (*FUNC_tjDestroy)(tjhandle handle); +typedef void (*FUNC_tjFree)(unsigned char *buffer); + +static bool write_buffer_to_file(const char *filepath, const unsigned char *data, unsigned long size) { + FILE *file = fopen(filepath, "wb"); + if(!file) + return false; + + const bool success = fwrite(data, size, 1, file) == 1; + fclose(file); + return success; +} + +static bool write_jpeg_with_libturbojpeg(const char *filepath, int width, int height, const void *data, int quality) { + void *libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY); + if(!libturbojpeg_lib) + return false; + + bool success = false; + const FUNC_tjInitCompress tjInitCompress_func = (FUNC_tjInitCompress)dlsym(libturbojpeg_lib, "tjInitCompress"); + const FUNC_tjCompress2 tjCompress2_func = (FUNC_tjCompress2)dlsym(libturbojpeg_lib, "tjCompress2"); + const FUNC_tjDestroy tjDestroy_func = (FUNC_tjDestroy)dlsym(libturbojpeg_lib, "tjDestroy"); + const FUNC_tjFree tjFree_func = (FUNC_tjFree)dlsym(libturbojpeg_lib, "tjFree"); + if(!tjInitCompress_func || !tjCompress2_func || !tjDestroy_func || !tjFree_func) { + dlclose(libturbojpeg_lib); + return false; + } + + tjhandle compressor = tjInitCompress_func(); + if(compressor) { + unsigned char *jpeg_data = NULL; + unsigned long jpeg_size = 0; + if(tjCompress2_func(compressor, data, width, width * 4, height, TJPF_RGBA, &jpeg_data, &jpeg_size, TJSAMP_420, quality, 0) == 0) + success = write_buffer_to_file(filepath, jpeg_data, jpeg_size); + + if(jpeg_data) + tjFree_func(jpeg_data); + tjDestroy_func(compressor); + } + + dlclose(libturbojpeg_lib); + return success; +} /* TODO: Support hdr/10-bit */ bool gsr_image_writer_init_opengl(gsr_image_writer *self, gsr_egl *egl, int width, int height) { @@ -40,7 +94,9 @@ static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const bool success = false; switch(image_format) { case GSR_IMAGE_FORMAT_JPEG: - success = stbi_write_jpg(filepath, self->width, self->height, 4, data, quality); + success = write_jpeg_with_libturbojpeg(filepath, self->width, self->height, data, quality); + if(!success) + success = stbi_write_jpg(filepath, self->width, self->height, 4, data, quality); break; case GSR_IMAGE_FORMAT_PNG: success = stbi_write_png(filepath, self->width, self->height, 4, data, 0); diff --git a/src/kde_night_light.c b/src/kde_night_light.c new file mode 100644 index 0000000..bed43af --- /dev/null +++ b/src/kde_night_light.c @@ -0,0 +1,213 @@ +#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 <stdio.h> +#include <stddef.h> + +gsr_kde_night_light* gsr_kde_night_light_create(void) { + fprintf(stderr, "gsr warning: kde night light handling disabled because gsr was compiled without pipewire (which also disables dbus)\n"); + 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 */ diff --git a/src/main.cpp b/src/main.cpp index d64d7f4..852a3c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1116,7 +1116,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename, for(const AudioTrack &audio_track : audio_tracks) { AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); - if(!audio_track.name.empty()) + if(!audio_track.name.empty() && !arg_parser.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", audio_track.name.c_str(), 0); avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); result.audio_inputs.push_back({&audio_track, audio_stream}); @@ -1375,7 +1375,10 @@ static MergedAudioInputs parse_audio_input_arg(const char *str) { AudioInput audio_input; audio_input.name.assign(sub, size); - if(string_starts_with(audio_input.name.c_str(), "app:")) { + if(string_starts_with(audio_input.name.c_str(), "name:")) { + result.custom_name = audio_input.name.substr(5); + return true; + } else if(string_starts_with(audio_input.name.c_str(), "app:")) { audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); audio_input.type = AudioInputType::APPLICATION; audio_input.inverted = false; @@ -2121,6 +2124,8 @@ static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region static gsr_kms_client kms_client; static bool kms_client_initialized = false; static gsr_kms_response kms_response; +static gsr_kde_night_light *kde_night_light = nullptr; +static bool kde_night_light_initialized = false; static gsr_cursor x11_cursor; static Display *x11_cursor_display = NULL; @@ -2147,11 +2152,17 @@ static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_eg _exit(kms_init_res < 0 ? 1 : kms_init_res); } + if(!kde_night_light_initialized && gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND) { + kde_night_light_initialized = true; + kde_night_light = gsr_kde_night_light_create(); + } + gsr_capture_kms_params kms_params; memset(&kms_params, 0, sizeof(kms_params)); kms_params.egl = egl; kms_params.x11_cursor = &x11_cursor; kms_params.kms_response = &kms_response; + kms_params.kde_night_light = kde_night_light; kms_params.display_to_capture = capture_source.name.c_str(); kms_params.record_cursor = arg_parser.record_cursor; kms_params.hdr = video_codec_is_hdr(arg_parser.video_codec); @@ -2635,11 +2646,15 @@ static void match_app_audio_input_to_available_apps(const std::vector<AudioInput } struct AudioTrackDescription { + std::optional<std::string> custom_name; std::vector<std::string> devices; std::vector<std::string> applications; bool app_inverse = false; std::string to_title() const { + if(custom_name.has_value()){ + return custom_name.value(); + } std::string title; if(!devices.empty()) { title += "Devices: "; @@ -2680,8 +2695,12 @@ static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &aud if(!audio_input || audio_input[0] == '\0') continue; - requested_audio_inputs.push_back(parse_audio_input_arg(audio_input)); + MergedAudioInputs merged_inputs = parse_audio_input_arg(audio_input); + requested_audio_inputs.push_back(merged_inputs); AudioTrackDescription audio_track_description; + if(merged_inputs.custom_name.has_value()){ + audio_track_description.custom_name = merged_inputs.custom_name; + } for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) { if(request_audio_input.type == AudioInputType::APPLICATION) { @@ -4070,7 +4089,7 @@ int main(int argc, char **argv) { fprintf(stderr, "gsr error: added too many audio sources\n"); } - if(audio_stream && !merged_audio_inputs.track_name.empty()) + if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0); open_audio(audio_codec_context, arg_parser.ffmpeg_audio_opts); @@ -4778,6 +4797,8 @@ int main(int argc, char **argv) { gsr_kms_client_deinit(&kms_client); } + gsr_kde_night_light_destroy(kde_night_light); + if(!arg_parser.is_replaying && arg_parser.recording_saved_script) run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "regular"); diff --git a/src/utils.c b/src/utils.c index abe8354..9010064 100644 --- a/src/utils.c +++ b/src/utils.c @@ -599,6 +599,12 @@ vec2i scale_keep_aspect_ratio(vec2i from, vec2i to) { from.x = from.y * width_height_ratio; } + /* An extreme aspect ratio can cause a dimension to truncate to 0, which the video encoder cant handle */ + if(from.x < 1) + from.x = 1; + if(from.y < 1) + from.y = 1; + return from; } diff --git a/src/window/wayland.c b/src/window/wayland.c index f7218b3..dc5dfc5 100644 --- a/src/window/wayland.c +++ b/src/window/wayland.c @@ -8,9 +8,11 @@ #include <stdlib.h> #include <string.h> #include <stdint.h> +#include <unistd.h> #include <wayland-client.h> #include <wayland-egl.h> #include "xdg-output-unstable-v1-client-protocol.h" +#include "color-management-v1-client-protocol.h" #define GSR_MAX_OUTPUTS 32 @@ -25,6 +27,12 @@ typedef struct { vec2i logical_size; int32_t transform; char *name; + struct wp_color_management_output_v1 *color_management_output; + struct wp_image_description_v1 *image_description; + struct wp_image_description_info_v1 *image_description_info; + uint32_t reference_luminance; /* cd/m² (nits) */ + uint32_t max_luminance; /* cd/m² (nits) */ + bool luminance_valid; } gsr_wayland_output; struct gsr_window_wayland { @@ -36,6 +44,7 @@ struct gsr_window_wayland { gsr_wayland_output outputs[GSR_MAX_OUTPUTS]; int num_outputs; struct zxdg_output_manager_v1 *xdg_output_manager; + struct wp_color_manager_v1 *color_manager; }; static void output_handle_geometry(void *data, struct wl_output *wl_output, @@ -99,6 +108,106 @@ static const struct wl_output_listener output_listener = { .description = output_handle_description, }; +static void image_description_info_handle_done(void *data, struct wp_image_description_info_v1 *info) { + gsr_wayland_output *output = data; + wp_image_description_info_v1_destroy(info); + output->image_description_info = NULL; + output->luminance_valid = output->reference_luminance > 0 || output->max_luminance > 0; +} + +static void image_description_info_handle_icc_file(void *data, struct wp_image_description_info_v1 *info, int32_t icc, uint32_t icc_size) { + (void)data; (void)info; (void)icc_size; + close(icc); +} + +static void image_description_info_handle_primaries(void *data, struct wp_image_description_info_v1 *info, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { + (void)data; (void)info; (void)r_x; (void)r_y; (void)g_x; (void)g_y; (void)b_x; (void)b_y; (void)w_x; (void)w_y; +} + +static void image_description_info_handle_uint_noop(void *data, struct wp_image_description_info_v1 *info, uint32_t value) { + (void)data; (void)info; (void)value; +} + +static void image_description_info_handle_luminances(void *data, struct wp_image_description_info_v1 *info, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum) { + (void)info; (void)min_lum; + gsr_wayland_output *output = data; + output->max_luminance = max_lum; + output->reference_luminance = reference_lum; +} + +static void image_description_info_handle_target_luminance(void *data, struct wp_image_description_info_v1 *info, uint32_t min_lum, uint32_t max_lum) { + (void)data; (void)info; (void)min_lum; (void)max_lum; +} + +static const struct wp_image_description_info_v1_listener image_description_info_listener = { + .done = image_description_info_handle_done, + .icc_file = image_description_info_handle_icc_file, + .primaries = image_description_info_handle_primaries, + .primaries_named = image_description_info_handle_uint_noop, + .tf_power = image_description_info_handle_uint_noop, + .tf_named = image_description_info_handle_uint_noop, + .luminances = image_description_info_handle_luminances, + .target_primaries = image_description_info_handle_primaries, + .target_luminance = image_description_info_handle_target_luminance, + .target_max_cll = image_description_info_handle_uint_noop, + .target_max_fall = image_description_info_handle_uint_noop, +}; + +static void image_description_handle_failed(void *data, struct wp_image_description_v1 *image_description, uint32_t cause, const char *msg) { + (void)cause; (void)msg; + gsr_wayland_output *output = data; + wp_image_description_v1_destroy(image_description); + output->image_description = NULL; +} + +static void image_description_handle_ready(void *data, struct wp_image_description_v1 *image_description, uint32_t identity) { + (void)identity; + gsr_wayland_output *output = data; + if(output->image_description_info) + wp_image_description_info_v1_destroy(output->image_description_info); + + output->image_description_info = wp_image_description_v1_get_information(image_description); + wp_image_description_info_v1_add_listener(output->image_description_info, &image_description_info_listener, output); + wp_image_description_v1_destroy(image_description); + output->image_description = NULL; +} + +static void image_description_handle_ready2(void *data, struct wp_image_description_v1 *image_description, uint32_t identity_hi, uint32_t identity_lo) { + (void)identity_lo; + image_description_handle_ready(data, image_description, identity_hi); +} + +static const struct wp_image_description_v1_listener image_description_listener = { + .failed = image_description_handle_failed, + .ready = image_description_handle_ready, + .ready2 = image_description_handle_ready2, +}; + +static void gsr_wayland_output_fetch_image_description(gsr_wayland_output *output) { + if(output->image_description_info) { + wp_image_description_info_v1_destroy(output->image_description_info); + output->image_description_info = NULL; + } + + if(output->image_description) { + wp_image_description_v1_destroy(output->image_description); + output->image_description = NULL; + } + + output->image_description = wp_color_management_output_v1_get_image_description(output->color_management_output); + wp_image_description_v1_add_listener(output->image_description, &image_description_listener, output); +} + +static void color_management_output_handle_image_description_changed(void *data, struct wp_color_management_output_v1 *color_management_output) { + (void)color_management_output; + gsr_wayland_output *output = data; + gsr_wayland_output_fetch_image_description(output); +} + +static const struct wp_color_management_output_v1_listener color_management_output_listener = { + .image_description_changed = color_management_output_handle_image_description_changed, +}; + 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 +249,11 @@ 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, wp_color_manager_v1_interface.name) == 0) { + if(window_wayland->color_manager) + return; + + window_wayland->color_manager = wl_registry_bind(registry, name, &wp_color_manager_v1_interface, 1); } } @@ -221,6 +335,22 @@ static void gsr_window_wayland_set_monitor_outputs_from_xdg_output(gsr_window_wa // return arg1->logical_pos.y - arg2->logical_pos.y; // } +static void gsr_window_wayland_setup_color_management_outputs(gsr_window_wayland *self) { + if(!self->color_manager) + return; + + for(int i = 0; i < self->num_outputs; ++i) { + gsr_wayland_output *output = &self->outputs[i]; + output->color_management_output = wp_color_manager_v1_get_output(self->color_manager, output->output); + wp_color_management_output_v1_add_listener(output->color_management_output, &color_management_output_listener, output); + gsr_wayland_output_fetch_image_description(output); + } + + // Fetch the image descriptions and then the image description infos + wl_display_roundtrip(self->display); + wl_display_roundtrip(self->display); +} + static void gsr_window_wayland_set_monitor_real_positions(gsr_window_wayland *self) { gsr_wayland_output *sorted_outputs[GSR_MAX_OUTPUTS]; for(int i = 0; i < self->num_outputs; ++i) { @@ -257,6 +387,26 @@ static void gsr_window_wayland_deinit(gsr_window_wayland *self) { } for(int i = 0; i < self->num_outputs; ++i) { + if(self->outputs[i].image_description_info) { + wp_image_description_info_v1_destroy(self->outputs[i].image_description_info); + self->outputs[i].image_description_info = NULL; + } + + if(self->outputs[i].image_description) { + wp_image_description_v1_destroy(self->outputs[i].image_description); + self->outputs[i].image_description = NULL; + } + + if(self->outputs[i].color_management_output) { + wp_color_management_output_v1_destroy(self->outputs[i].color_management_output); + self->outputs[i].color_management_output = NULL; + } + + if(self->outputs[i].xdg_output) { + zxdg_output_v1_destroy(self->outputs[i].xdg_output); + self->outputs[i].xdg_output = NULL; + } + if(self->outputs[i].output) { wl_output_destroy(self->outputs[i].output); self->outputs[i].output = NULL; @@ -266,14 +416,14 @@ static void gsr_window_wayland_deinit(gsr_window_wayland *self) { free(self->outputs[i].name); self->outputs[i].name = NULL; } - - if(self->outputs[i].xdg_output) { - zxdg_output_v1_destroy(self->outputs[i].xdg_output); - self->outputs[i].output = NULL; - } } self->num_outputs = 0; + if(self->color_manager) { + wp_color_manager_v1_destroy(self->color_manager); + self->color_manager = NULL; + } + if(self->xdg_output_manager) { zxdg_output_manager_v1_destroy(self->xdg_output_manager); self->xdg_output_manager = NULL; @@ -312,6 +462,7 @@ static bool gsr_window_wayland_init(gsr_window_wayland *self) { wl_display_roundtrip(self->display); gsr_window_wayland_set_monitor_outputs_from_xdg_output(self); + gsr_window_wayland_setup_color_management_outputs(self); gsr_window_wayland_set_monitor_real_positions(self); if(!self->compositor) { @@ -420,6 +571,20 @@ 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 < self->num_outputs; ++i) { + const gsr_wayland_output *output = &self->outputs[i]; + if(!output->name || strcmp(output->name, monitor_name) != 0 || !output->luminance_valid) + continue; + + hdr_info->sdr_white_luminance = output->reference_luminance; + hdr_info->max_peak_luminance = output->max_luminance; + return true; + } + return false; +} + gsr_window* gsr_window_wayland_create(void) { gsr_window *window = calloc(1, sizeof(gsr_window)); if(!window) @@ -445,6 +610,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; +} |
