Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/sway/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ sway_cmd output_cmd_color_profile;
sway_cmd output_cmd_disable;
sway_cmd output_cmd_dpms;
sway_cmd output_cmd_enable;
sway_cmd output_cmd_hdr;
sway_cmd output_cmd_max_render_time;
sway_cmd output_cmd_mode;
sway_cmd output_cmd_modeline;
Expand Down
3 changes: 2 additions & 1 deletion include/sway/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ enum scale_filter_mode {
};

enum render_bit_depth {
RENDER_BIT_DEPTH_DEFAULT, // the default is currently 8
RENDER_BIT_DEPTH_DEFAULT, // the default is currently 8 for SDR, 10 for HDR
RENDER_BIT_DEPTH_6,
RENDER_BIT_DEPTH_8,
RENDER_BIT_DEPTH_10,
Expand Down Expand Up @@ -291,6 +291,7 @@ struct output_config {
bool set_color_transform;
struct wlr_color_transform *color_transform;
int allow_tearing;
int hdr;

char *background;
char *background_option;
Expand Down
4 changes: 4 additions & 0 deletions include/sway/output.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ struct sway_output {
uint32_t refresh_nsec;
int max_render_time; // In milliseconds
struct wl_event_source *repaint_timer;

bool allow_tearing;
bool hdr;
};

struct sway_output_non_desktop {
Expand Down Expand Up @@ -129,6 +131,8 @@ struct sway_container *output_find_container(struct sway_output *output,

void output_get_box(struct sway_output *output, struct wlr_box *box);

bool output_supports_hdr(struct wlr_output *output, const char **unsupported_reason_ptr);

enum sway_container_layout output_get_default_layout(
struct sway_output *output);

Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pcre2 = dependency('libpcre2-8')
wayland_server = dependency('wayland-server', version: '>=1.21.0')
wayland_client = dependency('wayland-client')
wayland_cursor = dependency('wayland-cursor')
wayland_protos = dependency('wayland-protocols', version: '>=1.24', default_options: ['tests=false'])
wayland_protos = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false'])
xkbcommon = dependency('xkbcommon', version: '>=1.5.0')
cairo = dependency('cairo')
pango = dependency('pango')
Expand Down
1 change: 1 addition & 0 deletions protocols/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ wayland_scanner = find_program(
protocols = [
wl_protocol_dir / 'stable/tablet/tablet-v2.xml',
wl_protocol_dir / 'stable/xdg-shell/xdg-shell.xml',
wl_protocol_dir / 'staging/color-management/color-management-v1.xml',
wl_protocol_dir / 'staging/content-type/content-type-v1.xml',
wl_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml',
wl_protocol_dir / 'staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml',
Expand Down
1 change: 1 addition & 0 deletions sway/commands/output.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ static const struct cmd_handler output_handlers[] = {
{ "disable", output_cmd_disable },
{ "dpms", output_cmd_dpms },
{ "enable", output_cmd_enable },
{ "hdr", output_cmd_hdr },
{ "max_render_time", output_cmd_max_render_time },
{ "mode", output_cmd_mode },
{ "modeline", output_cmd_modeline },
Expand Down
37 changes: 37 additions & 0 deletions sway/commands/output/hdr.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include <strings.h>
#include "sway/commands.h"
#include "sway/config.h"
#include "sway/output.h"
#include "util.h"

struct cmd_results *output_cmd_hdr(int argc, char **argv) {
if (!config->handler_context.output_config) {
return cmd_results_new(CMD_FAILURE, "Missing output config");
}
if (argc == 0) {
return cmd_results_new(CMD_INVALID, "Missing hdr argument");
}

bool current = false;
if (strcasecmp(argv[0], "toggle") == 0) {
const char *oc_name = config->handler_context.output_config->name;
if (strcmp(oc_name, "*") == 0) {
return cmd_results_new(CMD_INVALID,
"Cannot apply toggle to all outputs");
}

struct sway_output *output = all_output_by_name_or_id(oc_name);
if (!output) {
return cmd_results_new(CMD_FAILURE,
"Cannot apply toggle to unknown output %s", oc_name);
}

current = output->hdr;
}

config->handler_context.output_config->hdr = parse_boolean(argv[0], current);

config->handler_context.leftovers.argc = argc - 1;
config->handler_context.leftovers.argv = argv + 1;
return NULL;
}
100 changes: 80 additions & 20 deletions sway/config/output.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ struct output_config *new_output_config(const char *name) {
oc->color_transform = NULL;
oc->power = -1;
oc->allow_tearing = -1;
oc->hdr = -1;
return oc;
}

Expand Down Expand Up @@ -154,6 +155,9 @@ static void supersede_output_config(struct output_config *dst, struct output_con
if (src->allow_tearing != -1) {
dst->allow_tearing = -1;
}
if (src->hdr != -1) {
dst->hdr = -1;
}
}

// merge_output_config sets all fields in dst that were set in src
Expand Down Expand Up @@ -229,6 +233,9 @@ static void merge_output_config(struct output_config *dst, struct output_config
if (src->allow_tearing != -1) {
dst->allow_tearing = src->allow_tearing;
}
if (src->hdr != -1) {
dst->hdr = src->hdr;
}
}

void store_output_config(struct output_config *oc) {
Expand Down Expand Up @@ -271,11 +278,11 @@ void store_output_config(struct output_config *oc) {

sway_log(SWAY_DEBUG, "Config stored for output %s (enabled: %d) (%dx%d@%fHz "
"position %d,%d scale %f subpixel %s transform %d) (bg %s %s) (power %d) "
"(max render time: %d) (allow tearing: %d)",
"(max render time: %d) (allow tearing: %d) (hdr: %d)",
oc->name, oc->enabled, oc->width, oc->height, oc->refresh_rate,
oc->x, oc->y, oc->scale, sway_wl_output_subpixel_to_string(oc->subpixel),
oc->transform, oc->background, oc->background_option, oc->power,
oc->max_render_time, oc->allow_tearing);
oc->max_render_time, oc->allow_tearing, oc->hdr);

// If the configuration was not merged into an existing configuration, add
// it to the list. Otherwise we're done with it and can free it.
Expand Down Expand Up @@ -341,6 +348,45 @@ static void set_modeline(struct wlr_output *output,
#endif
}

bool output_supports_hdr(struct wlr_output *output, const char **unsupported_reason_ptr) {
const char *unsupported_reason = NULL;
if (!(output->supported_primaries & WLR_COLOR_NAMED_PRIMARIES_BT2020)) {
unsupported_reason = "BT2020 primaries not supported by output";
} else if (!(output->supported_transfer_functions & WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ)) {
unsupported_reason = "PQ transfer function not supported by output";
} else if (!server.renderer->features.output_color_transform) {
unsupported_reason = "renderer doesn't support output color transforms";
}
if (unsupported_reason_ptr != NULL) {
*unsupported_reason_ptr = unsupported_reason;
}
return unsupported_reason == NULL;
}

static void set_hdr(struct wlr_output *output, struct wlr_output_state *pending, bool enabled) {
const char *unsupported_reason = NULL;
if (!output_supports_hdr(output, &unsupported_reason)) {
sway_log(SWAY_ERROR, "Cannot enable HDR on output %s: %s",
output->name, unsupported_reason);
enabled = false;
}

if (!enabled) {
if (output->supported_primaries != 0 || output->supported_transfer_functions != 0) {
sway_log(SWAY_DEBUG, "Disabling HDR on output %s", output->name);
wlr_output_state_set_image_description(pending, NULL);
}
return;
}

sway_log(SWAY_DEBUG, "Enabling HDR on output %s", output->name);
const struct wlr_output_image_description image_desc = {
.primaries = WLR_COLOR_NAMED_PRIMARIES_BT2020,
.transfer_function = WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ,
};
wlr_output_state_set_image_description(pending, &image_desc);
}

/* Some manufacturers hardcode the aspect-ratio of the output in the physical
* size field. */
static bool phys_size_is_aspect_ratio(struct wlr_output *output) {
Expand Down Expand Up @@ -415,6 +461,16 @@ static enum render_bit_depth bit_depth_from_format(uint32_t render_format) {
return RENDER_BIT_DEPTH_DEFAULT;
}

static enum render_bit_depth get_config_render_bit_depth(const struct output_config *oc) {
if (oc && oc->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) {
return oc->render_bit_depth;
}
if (oc && oc->hdr == 1) {
return RENDER_BIT_DEPTH_10;
}
return RENDER_BIT_DEPTH_8;
}

static bool render_format_is_bgr(uint32_t fmt) {
return fmt == DRM_FORMAT_XBGR2101010 || fmt == DRM_FORMAT_XBGR8888;
}
Expand Down Expand Up @@ -485,24 +541,29 @@ static void queue_output_config(struct output_config *oc,
}
}

if (oc && oc->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) {
if (oc->render_bit_depth == RENDER_BIT_DEPTH_10 &&
bit_depth_from_format(output->wlr_output->render_format) == oc->render_bit_depth) {
// 10-bit was set successfully before, try to save some tests by reusing the format
wlr_output_state_set_render_format(pending, output->wlr_output->render_format);
} else if (oc->render_bit_depth == RENDER_BIT_DEPTH_10) {
wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB2101010);
} else if (oc->render_bit_depth == RENDER_BIT_DEPTH_6){
wlr_output_state_set_render_format(pending, DRM_FORMAT_RGB565);
} else {
wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB8888);
}
enum render_bit_depth render_bit_depth = get_config_render_bit_depth(oc);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defaults to 10-bit even if set_hdr would bail out on requesting HDR. Maybe we should flip things, setting HDR and changing the default based on whether HDR was set or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that sounds complicated to implement because get_config_render_bit_depth() is called from elsewhere too, e.g. search_render_format(). Also I think it's better to keep things simple to make the render bit depth default only depend on the config alone rather than the renderer/output capabilities.

Copy link
Member

@kennylevinsen kennylevinsen Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does feel a little bit surprising that output * hdr on (where someone just wants to use HDR whenever available) would lead to all non-HDR displays having 10-bit render depth, especially as you cannot easily revert it (output * render_bit_depth 8 would also impact HDR-capable displays), and as 10-bit on Vulkan would activate the blending buffer which currently triples VRAM use and degrades performance a bit.

On the other hand I doubt anyone needs output * hdr on, at least right now where HDR is very much hit and miss. We can probably just ignore this for now.

get_config_render_bit_depth() is called from elsewhere too, e.g. search_render_format()

Both uses would be able to take wlr_output_state as argument if one wanted to check if hdr is enabled to select default render bit depth. search_render_format is called after queue_output_config has filled out the state, and HDR is not currently part of the output config search so it stays set until we give up.

if (render_bit_depth == RENDER_BIT_DEPTH_10 &&
bit_depth_from_format(output->wlr_output->render_format) == render_bit_depth) {
// 10-bit was set successfully before, try to save some tests by reusing the format
wlr_output_state_set_render_format(pending, output->wlr_output->render_format);
} else if (render_bit_depth == RENDER_BIT_DEPTH_10) {
wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB2101010);
} else if (render_bit_depth == RENDER_BIT_DEPTH_6) {
wlr_output_state_set_render_format(pending, DRM_FORMAT_RGB565);
} else {
wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB8888);
}

bool hdr = oc && oc->hdr == 1;
if (hdr && oc->color_transform != NULL) {
sway_log(SWAY_ERROR, "Cannot HDR on output %s: output has an ICC profile set", wlr_output->name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should log from here, queue_output_config is called all the the time and should just produce the current wlr_output_state. Same applies to set_hdr - if someone does output * hdr on, getting "PQ transfer function not supported by output" errors one or more times for every output not supporting HDR, every time any output changes is a bit noisy...

Maybe we could log something about output capabilities (HDR and VRR for example) when we create the output.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already log for other errors, e.g. mode not found, invalid modeline etc.

I think it's important to log something when the user tries to enable HDR but we can't. Logging on startup isn't enough IMHO.

The current scheme only logs when the output configuration changes, which isn't too spammy and easy enough for users to fix if they want to (by only enabling HDR if the output supports it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only logs when the output configuration changes

Multiple times on every change. I really don't like log noise. :/

I have slowly been chipping away at it (e.g., a630272), removing most bogus logging and collecting meaningful stuff in places like dump_output_state instead of just whenever we construct a wlr_output_state. The only remaining logs in queue_output_config is set_mode stating fallback to preferred mode and set_modeline failing if called on a non-drm output, which I didn't have an immediate replacement for...

The HDR case is logically similar to the adaptive sync case where the setting is best effort and can be disabled by display, link or connector restrictions, in which case not enabling is not considered an error. I do think in that case that we need to clarify what the output capabilities are in the log, as we right now only communicate what we end up doing (with debug log level).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the adaptive sync thing and was about to write a patch to add a log there. I find it pretty confusing that a command like output DP-1 adaptive_sync on would silently do nothing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the kind of INFO-level logging I was thinking of: #8784

I feel like that's clearer than error logging. We could consider also logging the final output state as INFO though, right now that's only debug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that kind of logging is very helpful to users TBH.

Copy link
Member

@kennylevinsen kennylevinsen Jun 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's far more helpful than sporadic error messages. Errors is something people try to fix as if something is wrong that has to be corrected, and there's nothing to tell if something isn't meant to work.

It's much easier to know if something is supported and to troubleshoot if the first step is: Does it say "yes"? Both for users, those providing support and those writing various FAQs/wikis for self-help.

But I won't hold up the feature for that further.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the config tries to enable HDR10 and HDR10 cannot be enabled, I think it's only fair to print an error.

Anyways, it doesn't seem like we're going to come to an agreement here.

hdr = false;
}
set_hdr(wlr_output, pending, hdr);
}

static bool finalize_output_config(struct output_config *oc, struct sway_output *output) {
static bool finalize_output_config(struct output_config *oc, struct sway_output *output,
const struct wlr_output_state *applied) {
if (output == root->fallback_output) {
return false;
}
Expand Down Expand Up @@ -561,6 +622,7 @@ static bool finalize_output_config(struct output_config *oc, struct sway_output

output->max_render_time = oc && oc->max_render_time > 0 ? oc->max_render_time : 0;
output->allow_tearing = oc && oc->allow_tearing > 0;
output->hdr = applied->image_description != NULL;

return true;
}
Expand Down Expand Up @@ -785,10 +847,7 @@ static bool search_render_format(struct search_context *ctx, size_t output_idx)

const struct wlr_drm_format_set *primary_formats =
wlr_output_get_primary_formats(wlr_output, server.allocator->buffer_caps);
enum render_bit_depth needed_bits = RENDER_BIT_DEPTH_8;
if (cfg->config && cfg->config->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) {
needed_bits = cfg->config->render_bit_depth;
}
enum render_bit_depth needed_bits = get_config_render_bit_depth(cfg->config);
for (size_t idx = 0; fmts[idx] != DRM_FORMAT_INVALID; idx++) {
enum render_bit_depth format_bits = bit_depth_from_format(fmts[idx]);
if (needed_bits < format_bits) {
Expand Down Expand Up @@ -943,9 +1002,10 @@ static bool apply_resolved_output_configs(struct matched_output_config *configs,

for (size_t idx = 0; idx < configs_len; idx++) {
struct matched_output_config *cfg = &configs[idx];
struct wlr_backend_output_state *backend_state = &states[idx];
sway_log(SWAY_DEBUG, "Finalizing config for %s",
cfg->output->wlr_output->name);
finalize_output_config(cfg->config, cfg->output);
finalize_output_config(cfg->config, cfg->output, &backend_state->base);
}

// Output layout being applied in finalize_output_config can shift outputs
Expand Down
4 changes: 3 additions & 1 deletion sway/ipc-json.c
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ static void ipc_json_describe_wlr_output(struct wlr_output *wlr_output, json_obj
json_object_object_add(features_object, "adaptive_sync",
json_object_new_boolean(wlr_output->adaptive_sync_supported ||
wlr_output->adaptive_sync_status == WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED));
json_object_object_add(features_object, "hdr",
json_object_new_boolean(output_supports_hdr(wlr_output, NULL)));
json_object_object_add(object, "features", features_object);
}

Expand Down Expand Up @@ -406,8 +408,8 @@ static void ipc_json_describe_enabled_output(struct sway_output *output,
}

json_object_object_add(object, "max_render_time", json_object_new_int(output->max_render_time));

json_object_object_add(object, "allow_tearing", json_object_new_boolean(output->allow_tearing));
json_object_object_add(object, "hdr", json_object_new_boolean(output->hdr));
}

json_object *ipc_json_describe_disabled_output(struct sway_output *output) {
Expand Down
1 change: 1 addition & 0 deletions sway/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ sway_sources = files(
'commands/output/disable.c',
'commands/output/dpms.c',
'commands/output/enable.c',
'commands/output/hdr.c',
'commands/output/max_render_time.c',
'commands/output/mode.c',
'commands/output/position.c',
Expand Down
28 changes: 28 additions & 0 deletions sway/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <wlr/render/allocator.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_alpha_modifier_v1.h>
#include <wlr/types/wlr_color_management_v1.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_content_type_v1.h>
#include <wlr/types/wlr_cursor_shape_v1.h>
Expand Down Expand Up @@ -443,6 +444,33 @@ bool server_init(struct sway_server *server) {
server->request_set_cursor_shape.notify = handle_request_set_cursor_shape;
wl_signal_add(&cursor_shape_manager->events.request_set_shape, &server->request_set_cursor_shape);

if (server->renderer->features.input_color_transform) {
const enum wp_color_manager_v1_render_intent render_intents[] = {
WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL,
};
const enum wp_color_manager_v1_transfer_function transfer_functions[] = {
WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB,
WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ,
WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR,
};
const enum wp_color_manager_v1_primaries primaries[] = {
WP_COLOR_MANAGER_V1_PRIMARIES_SRGB,
WP_COLOR_MANAGER_V1_PRIMARIES_BT2020,
};
wlr_color_manager_v1_create(server->wl_display, 1, &(struct wlr_color_manager_v1_options){
.features = {
.parametric = true,
.set_mastering_display_primaries = true,
},
.render_intents = render_intents,
.render_intents_len = sizeof(render_intents) / sizeof(render_intents[0]),
.transfer_functions = transfer_functions,
.transfer_functions_len = sizeof(transfer_functions) / sizeof(transfer_functions[0]),
.primaries = primaries,
.primaries_len = sizeof(primaries) / sizeof(primaries[0]),
});
}

wl_list_init(&server->pending_launcher_ctxs);

// Avoid using "wayland-0" as display socket
Expand Down
3 changes: 3 additions & 0 deletions sway/sway-ipc.7.scd
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ following properties:
|- rect
: object
: The bounds for the output consisting of _x_, _y_, _width_, and _height_
|- hdr
: boolean
: Whether HDR is enabled


*Example Reply:*
Expand Down
11 changes: 11 additions & 0 deletions sway/sway-output.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ must be separated by one space. For example:

This setting only has effect when a window is fullscreen on the output.

*output* <name> hdr on|off|toggle
Enables or disables HDR (High Dynamic Range). HDR enables a larger color
gamut and brightness range. HDR uses the BT2020 primaries and the PQ
transfer function.

When HDR is enabled, _render_bit_depth_ is implicitly set to 10 unless
explicitly configured. Using a lower render bit depth may result in color
banding artifacts.

HDR needs to be supported by the output and renderer to be enabled.

# SEE ALSO

*sway*(5) *sway-input*(5)
Loading