// Copyright 2022 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "sommelier.h" // NOLINT(build/include_directory) #include "sommelier-transform.h" // NOLINT(build/include_directory) #include #include #include #include #include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) #include "xdg-output-unstable-v1-client-protocol.h" // NOLINT(build/include_directory) #define MAX_OUTPUT_SCALE 2 #define INCH_IN_MM 25.4 // Legacy X11 applications use DPI to decide on their scale. This value is what // the convention for a "normal" scale is. One way to verify the convention is // to note the DPI of a typical monitor circa ~2005, i.e. 20" 1080p. #define DEFACTO_DPI 96 double sl_output_aura_scale_factor_to_double(int scale_factor) { // Aura scale factor is an enum that for all currently know values // is a scale value multipled by 1000. For example, enum value for // 1.25 scale factor is 1250. return scale_factor / 1000.0; } int dpi_to_physical_mm(double dpi, int px) { return px * (INCH_IN_MM / dpi); } void sl_output_get_host_output_state(struct sl_host_output* host, int* scale, int* physical_width, int* physical_height, int* width, int* height) { // The user's chosen zoom level. double current_scale = sl_output_aura_scale_factor_to_double(host->current_scale); // The scale applied to a screen at the default zoom. I.e. this value // determines the meaning of "100%" zoom, and how zoom relates to the // apparent resolution: // // apparent_res = native_res / device_scale_factor * current_scale // // e.g.: On a device with a DSF of 2.0, 80% zoom really means "apply 1.6x // scale", and 50% zoom would give you an apparent resolution equal to the // native one. double device_scale_factor = sl_output_aura_scale_factor_to_double(host->device_scale_factor); // Optimistically, we will try to apply the scale that the user chose. // Failing that, we will use the scale set for this wl_output. double applied_scale = device_scale_factor * current_scale; if (!host->ctx->aura_shell) { applied_scale = host->scale_factor; } int target_dpi = DEFACTO_DPI; if (host->ctx->xwayland) { // For X11, we must fix the scale to be 1 (since X apps typically can't // handle scaling). As a result, we adjust the resolution (based on the // scale we want to apply and sommelier's configuration) and the physical // dimensions (based on what DPI we want the applications to use). E.g.: // - Device scale is 1.25x, with 1920x1080 resolution on a 295mm by 165mm // screen. // - User chosen zoom is 130% // - Sommelier is scaled to 0.5 (a.k.a low density). Since ctx->scale also // has the device scale, it will be 0.625 (i.e. 0.5 * 1.25). // - We want the DPI to be 120 (i.e. 96 * 1.25) // - Meaning 0.21 mm/px // - We report resolution 738x415 (1920x1080 * 0.5 / 1.3) // - We report dimensions 155mm by 87mm (738x415 * 0.21) // This is mostly expected, another way of thinking about them is that zoom // and scale modify the application's understanding of length: // - Increasing the zoom makes lengths appear longer (i.e. fewer mm to work // with over the same real length). // - Scaling the screen does the inverse. if (scale) *scale = 1; *width = host->width * host->ctx->scale / applied_scale; *height = host->height * host->ctx->scale / applied_scale; target_dpi = DEFACTO_DPI * device_scale_factor; *physical_width = dpi_to_physical_mm(target_dpi, *width); *physical_height = dpi_to_physical_mm(target_dpi, *height); } else { // For wayland, we directly apply the scale which combines the user's chosen // preference (from aura) and the scale which this sommelier was configured // for (i.e. based on ctx->scale, which comes from the env/cmd line). // // See above comment: ctx->scale already has the device_scale_factor in it, // so this maths actually looks like: // // applied / ctx->scale // = (current*DSF) / (config*DSF) // = current / config // // E.g. if we configured sommelier to scale everything 0.5x, and the user // has chosen 130% zoom, we are applying 2.6x scale factor. int s = MIN(ceil(applied_scale / host->ctx->scale), MAX_OUTPUT_SCALE); if (scale) *scale = s; *physical_width = host->physical_width; *physical_height = host->physical_height; *width = host->width * host->ctx->scale * s / applied_scale; *height = host->height * host->ctx->scale * s / applied_scale; target_dpi = (*width * INCH_IN_MM) / *physical_width; } if (host->ctx->dpi.size) { int adjusted_dpi = *(reinterpret_cast(host->ctx->dpi.data)); // Choose the DPI bucket which is closest to the target DPI which we // calculated above. int* dpi; sl_array_for_each(dpi, &host->ctx->dpi) { if (abs(*dpi - target_dpi) < abs(adjusted_dpi - target_dpi)) adjusted_dpi = *dpi; } *physical_width = dpi_to_physical_mm(adjusted_dpi, *width); *physical_height = dpi_to_physical_mm(adjusted_dpi, *height); } } void sl_output_get_logical_dimensions(struct sl_host_output* host, bool rotated, int32_t* width, int32_t* height) { if (rotated) { // Pass the dimensions as is (it could be rotated) *width = host->logical_width; *height = host->logical_height; } else { // The transform here indicates how a window image will be // rotated when composited. The incoming surface from the // application will NOT have its dimensions rotated. // For this reason, in order to calculate the scale factors // for direct scale, we will need the non rotated logical // dimensions. switch (host->transform) { case WL_OUTPUT_TRANSFORM_NORMAL: case WL_OUTPUT_TRANSFORM_180: case WL_OUTPUT_TRANSFORM_FLIPPED: case WL_OUTPUT_TRANSFORM_FLIPPED_180: *width = host->logical_width; *height = host->logical_height; break; default: *width = host->logical_height; *height = host->logical_width; break; } } } void sl_output_init_dimensions_direct(struct sl_host_output* host, int* out_scale, int* out_physical_width, int* out_physical_height, int* out_width, int* out_height) { int32_t virtual_width = host->width; int32_t virtual_height = host->height; // This requires xdg_output_manager, it is assumed that it will be // available and we will have an appropriate set of logical dimensions // for this particular output. assert(host->ctx->viewporter); assert(host->ctx->xdg_output_manager); // The virtual width/height is computed by this function here based // on the physical width/height sl_transform_output_dimensions(host->ctx, &virtual_width, &virtual_height); host->virt_scale_x = static_cast(virtual_width) / host->width; host->virt_scale_y = static_cast(virtual_height) / host->height; *out_width = virtual_width; *out_height = virtual_height; // Force the scale to 1 // // This is reported to the guest through the wl_output protocol. // This value will signal by how much a compositor will upscale // all buffers by (1 is no scale). *out_scale = 1; // The physical dimensions (in mm) are the same, regardless // of the provided scale factor. *out_physical_width = host->physical_width; *out_physical_height = host->physical_height; // Retrieve the logical dimensions int32_t logical_width, logical_height; sl_output_get_logical_dimensions(host, /*rotated=*/false, &logical_width, &logical_height); // We want to be able to transform from virtual to XDG logical // coordinates // Virt to XDG -> div // XDG to Virt -> mul host->xdg_scale_x = static_cast(virtual_width) / static_cast(logical_width); host->xdg_scale_y = static_cast(virtual_height) / static_cast(logical_height); if (host->internal) { host->ctx->virt_scale_x = host->virt_scale_x; host->ctx->virt_scale_y = host->virt_scale_y; host->ctx->xdg_scale_x = host->xdg_scale_x; host->ctx->xdg_scale_y = host->xdg_scale_y; } } void sl_output_get_dimensions_original(struct sl_host_output* host, int* out_scale, int* out_physical_width, int* out_physical_height, int* out_width, int* out_height) { int scale; int physical_width; int physical_height; int width; int height; sl_output_get_host_output_state(host, &scale, &physical_width, &physical_height, &width, &height); // Use density of internal display for all Xwayland outputs. X11 clients // typically lack support for dynamically changing density so it's // preferred to always use the density of the internal display. if (host->ctx->xwayland) { struct sl_host_output* output; wl_list_for_each(output, &host->ctx->host_outputs, link) { if (output->internal) { int internal_width; int internal_height; sl_output_get_host_output_state(output, NULL, &physical_width, &physical_height, &internal_width, &internal_height); physical_width = (physical_width * width) / internal_width; physical_height = (physical_height * height) / internal_height; break; } } } *out_scale = scale; *out_physical_width = physical_width; *out_physical_height = physical_height; *out_width = width; *out_height = height; } // Recalculates the virt_x coordinates of outputs when an output is // add/removed/changed. skip_host is false if the host is being removed // (as it should not exist in the list already) and true if it's being // added/changed. void sl_output_shift_output_x(struct sl_host_output* host, bool skip_host) { // Outputs are positioned in a line from left to right ordered base on its // x position. struct sl_host_output* output; int next_output_x = 0; wl_list_for_each(output, &host->ctx->host_outputs, link) { if (output->virt_x != next_output_x) { output->virt_x = next_output_x; // Skipping sending current output's details here if skip host is set // as they are sent after this method. if (!skip_host || output != host) { // scaled_physical_width/height may have not been set yet. if (!output->scaled_physical_width || !output->scaled_physical_height) { int scale; int physical_width; int physical_height; int width; int height; if (host->ctx->use_direct_scale) { sl_output_init_dimensions_direct(host, &scale, &physical_width, &physical_height, &width, &height); } else { sl_output_get_dimensions_original(host, &scale, &physical_width, &physical_height, &width, &height); } host->scaled_physical_width = physical_width; host->scaled_physical_height = physical_height; } wl_output_send_geometry( output->resource, output->virt_x, output->virt_y, output->scaled_physical_width, output->scaled_physical_height, output->subpixel, output->make, output->model, output->transform); if (wl_resource_get_version(output->resource) >= WL_OUTPUT_DONE_SINCE_VERSION) wl_output_send_done(output->resource); } } next_output_x += output->width; } } void sl_output_send_host_output_state(struct sl_host_output* host) { int scale; int physical_width; int physical_height; int width; int height; if (host->ctx->use_direct_scale) { sl_output_init_dimensions_direct(host, &scale, &physical_width, &physical_height, &width, &height); } else { sl_output_get_dimensions_original(host, &scale, &physical_width, &physical_height, &width, &height); } host->scaled_physical_width = physical_width; host->scaled_physical_height = physical_height; // Shift all outputs that are to the right of host to the right if needed. sl_output_shift_output_x(host, true); host->virt_y = 0; wl_output_send_geometry(host->resource, host->virt_x, host->virt_y, host->scaled_physical_width, host->scaled_physical_height, host->subpixel, host->make, host->model, host->transform); wl_output_send_mode(host->resource, host->flags | WL_OUTPUT_MODE_CURRENT, width, height, host->refresh); if (wl_resource_get_version(host->resource) >= WL_OUTPUT_SCALE_SINCE_VERSION) wl_output_send_scale(host->resource, scale); if (wl_resource_get_version(host->resource) >= WL_OUTPUT_DONE_SINCE_VERSION) wl_output_send_done(host->resource); } static void sl_output_geometry(void* data, struct wl_output* output, int x, int y, int physical_width, int physical_height, int subpixel, const char* make, const char* model, int transform) { struct sl_host_output* host = static_cast(wl_output_get_user_data(output)); host->x = x; host->y = y; host->physical_width = physical_width; host->physical_height = physical_height; host->subpixel = subpixel; free(host->model); host->model = strdup(model); free(host->make); host->make = strdup(make); host->transform = transform; // host_outputs is sorted by x. Delete then re-insert at the correct position. wl_list_remove(&host->link); // Insert at the end by default. If insert_at is not set in the loop, // hosts's x is larger than all the ones in the list currently. struct wl_list* insert_at = host->ctx->host_outputs.prev; struct sl_host_output* iter; wl_list_for_each(iter, &host->ctx->host_outputs, link) { if (host->x < iter->x) { // This is the first output whose x cooridinate is to the right of host, // therefore insert to the left of it. insert_at = iter->link.prev; break; } } wl_list_insert(insert_at, &host->link); } static void sl_output_mode(void* data, struct wl_output* output, uint32_t flags, int width, int height, int refresh) { struct sl_host_output* host = static_cast(wl_output_get_user_data(output)); host->flags = flags; host->width = width; host->height = height; host->refresh = refresh; } static void sl_output_done(void* data, struct wl_output* output) { struct sl_host_output* host = static_cast(wl_output_get_user_data(output)); // Early out if scale is expected but not yet know. if (host->expecting_scale) return; sl_output_send_host_output_state(host); // Expect scale if aura output exists. if (host->aura_output) host->expecting_scale = 1; } static void sl_output_scale(void* data, struct wl_output* output, int32_t scale_factor) { struct sl_host_output* host = static_cast(wl_output_get_user_data(output)); host->scale_factor = scale_factor; } static const struct wl_output_listener sl_output_listener = { sl_output_geometry, sl_output_mode, sl_output_done, sl_output_scale}; static void sl_aura_output_scale(void* data, struct zaura_output* output, uint32_t flags, uint32_t scale) { struct sl_host_output* host = static_cast(zaura_output_get_user_data(output)); if (flags & ZAURA_OUTPUT_SCALE_PROPERTY_CURRENT) host->current_scale = scale; if (flags & ZAURA_OUTPUT_SCALE_PROPERTY_PREFERRED) host->preferred_scale = scale; host->expecting_scale = 0; } static void sl_aura_output_connection(void* data, struct zaura_output* output, uint32_t connection) { struct sl_host_output* host = static_cast(zaura_output_get_user_data(output)); host->internal = connection == ZAURA_OUTPUT_CONNECTION_TYPE_INTERNAL; } static void sl_aura_output_device_scale_factor(void* data, struct zaura_output* output, uint32_t device_scale_factor) { struct sl_host_output* host = static_cast(zaura_output_get_user_data(output)); host->device_scale_factor = device_scale_factor; } static void sl_aura_output_insets(void* data, struct zaura_output* output, int top, int left, int bottom, int right) {} static void sl_aura_output_logical_transform(void* data, struct zaura_output* output, int transform) {} static const struct zaura_output_listener sl_aura_output_listener = { sl_aura_output_scale, sl_aura_output_connection, sl_aura_output_device_scale_factor, sl_aura_output_insets, sl_aura_output_logical_transform}; static void sl_destroy_host_output(struct wl_resource* resource) { struct sl_host_output* host = static_cast(wl_resource_get_user_data(resource)); if (host->aura_output) zaura_output_destroy(host->aura_output); if (wl_output_get_version(host->proxy) >= WL_OUTPUT_RELEASE_SINCE_VERSION) { wl_output_release(host->proxy); } else { wl_output_destroy(host->proxy); } wl_resource_set_user_data(resource, NULL); wl_list_remove(&host->link); free(host->make); free(host->model); // Shift all outputs to the right of the deleted output to the left. sl_output_shift_output_x(host, false); delete host; } static void sl_xdg_output_logical_position( void* data, struct zxdg_output_v1* zxdg_output_v1, int32_t x, int32_t y) { struct sl_host_output* host = static_cast( zxdg_output_v1_get_user_data(zxdg_output_v1)); host->logical_y = y; host->logical_x = x; } static void sl_xdg_output_logical_size(void* data, struct zxdg_output_v1* zxdg_output_v1, int32_t width, int32_t height) { struct sl_host_output* host = static_cast( zxdg_output_v1_get_user_data(zxdg_output_v1)); host->logical_width = width; host->logical_height = height; host->expecting_logical_size = false; } static void sl_xdg_output_done(void* data, struct zxdg_output_v1* zxdg_output_v1) {} static void sl_xdg_output_name(void* data, struct zxdg_output_v1* zxdg_output_v1, const char* name) {} static void sl_xdg_output_desc(void* data, struct zxdg_output_v1* zxdg_output_v1, const char* desc) {} static const struct zxdg_output_v1_listener sl_xdg_output_listener = { sl_xdg_output_logical_position, sl_xdg_output_logical_size, sl_xdg_output_done, sl_xdg_output_name, sl_xdg_output_desc}; static void sl_bind_host_output(struct wl_client* client, void* data, uint32_t version, uint32_t id) { struct sl_output* output = (struct sl_output*)data; struct sl_context* ctx = output->ctx; struct sl_host_output* host = new sl_host_output(); host->ctx = ctx; host->resource = wl_resource_create(client, &wl_output_interface, MIN(version, output->version), id); wl_resource_set_implementation(host->resource, NULL, host, sl_destroy_host_output); host->proxy = static_cast(wl_registry_bind( wl_display_get_registry(ctx->display), output->id, &wl_output_interface, wl_resource_get_version(host->resource))); wl_output_add_listener(host->proxy, &sl_output_listener, host); output->host_output = host; host->aura_output = NULL; // We assume that first output is internal by default. host->internal = wl_list_empty(&ctx->host_outputs); host->x = 0; host->y = 0; host->virt_x = 0; host->virt_y = 0; host->logical_x = 0; host->logical_y = 0; host->physical_width = 0; host->physical_height = 0; host->scaled_physical_width = 0; host->scaled_physical_height = 0; host->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; host->make = strdup("unknown"); host->model = strdup("unknown"); host->transform = WL_OUTPUT_TRANSFORM_NORMAL; host->flags = 0; host->width = 1024; host->height = 768; host->logical_width = 1024; host->logical_height = 768; host->refresh = 60000; host->scale_factor = 1; host->current_scale = 1000; host->preferred_scale = 1000; host->device_scale_factor = 1000; host->expecting_scale = 0; host->expecting_logical_size = false; wl_list_insert(ctx->host_outputs.prev, &host->link); if (ctx->aura_shell) { host->expecting_scale = 1; host->internal = 0; host->aura_output = zaura_shell_get_aura_output(ctx->aura_shell->internal, host->proxy); zaura_output_add_listener(host->aura_output, &sl_aura_output_listener, host); } if (ctx->xdg_output_manager) { host->expecting_logical_size = true; host->zxdg_output = zxdg_output_manager_v1_get_xdg_output( ctx->xdg_output_manager->internal, host->proxy); zxdg_output_v1_add_listener(host->zxdg_output, &sl_xdg_output_listener, host); } } struct sl_global* sl_output_global_create(struct sl_output* output) { return sl_global_create(output->ctx, &wl_output_interface, output->version, output, sl_bind_host_output); }