blob: 305afd28f751e9b6f1b8b792bc35cbd94a60b2be [file] [log] [blame]
// 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 <algorithm>
#include <assert.h>
#include <cstdint>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <wayland-client.h>
#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_apply_rotation(
int transform, int width, int height, int* out_width, int* out_height) {
switch (transform) {
case WL_OUTPUT_TRANSFORM_NORMAL:
case WL_OUTPUT_TRANSFORM_180:
case WL_OUTPUT_TRANSFORM_FLIPPED:
case WL_OUTPUT_TRANSFORM_FLIPPED_180:
*out_width = width;
*out_height = height;
break;
default:
*out_width = height;
*out_height = width;
break;
}
}
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<int*>(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.
sl_output_apply_rotation(host->transform, host->logical_width,
host->logical_height, width, height);
}
}
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<double>(virtual_width) / host->width;
host->virt_scale_y = static_cast<double>(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<double>(virtual_width) / static_cast<double>(logical_width);
host->xdg_scale_y =
static_cast<double>(virtual_height) / static_cast<double>(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) {
for (auto output : host->ctx->host_outputs) {
if (output->internal) {
int internal_width;
int internal_height;
sl_output_get_host_output_state(output, nullptr, &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.
void sl_output_update_output_x(struct sl_context* ctx) {
// Outputs are positioned in a line from left to right based off their x
// position.
int next_output_x = 0;
for (auto output : ctx->host_outputs) {
// Update the value and mark for sending.
if (output->virt_x != next_output_x) {
output->virt_x = next_output_x;
output->needs_update = true;
}
next_output_x += output->virt_rotated_width;
}
}
struct sl_host_output* sl_infer_output_for_host_position(struct sl_context* ctx,
int32_t host_x,
int32_t host_y) {
struct sl_host_output* closest = nullptr;
int32_t closest_distance = INT32_MAX;
// Return the output containing, or closest to, the query X/Y coordinates
// in host logical space. "Closest" considers Manhattan distance.
for (auto output : ctx->host_outputs) {
if (!closest) {
closest = output;
}
int32_t x_distance;
if (host_x < output->x) {
// Query point is left of the output
x_distance = output->x - host_x;
} else if (host_x < output->x + output->width) {
// Query point is inside the output (on X axis)
x_distance = 0;
} else {
// Query point is right of the output
x_distance = host_x - (output->x + output->width);
}
int32_t y_distance;
if (host_y < output->y) {
// Query point is above the output
y_distance = output->y - host_y;
} else if (host_y < output->y + output->height) {
// Query point is inside the output (on Y axis)
y_distance = 0;
} else {
// Query point is below the output
y_distance = host_y - (output->y + output->height);
}
if (x_distance + y_distance < closest_distance) {
closest = output;
closest_distance = x_distance + y_distance;
if (closest_distance == 0) {
break;
}
}
}
return closest;
}
struct sl_host_output* sl_infer_output_for_guest_position(
struct sl_context* ctx, int32_t virt_x, int32_t virt_y) {
struct sl_host_output* first = nullptr;
struct sl_host_output* last = nullptr;
// Return the output containing the query X coordinate (in virtual space).
// Since outputs are placed in a horizontal line in virtual space, we can
// ignore the Y coordinate entirely.
for (auto output : ctx->host_outputs) {
if (!first) {
first = output;
}
last = output;
if (virt_x >= output->virt_x && virt_x < output->virt_x + output->width) {
return output;
}
}
// The query X coordinate is out of bounds, so return the "nearest" output.
if (first && virt_x < first->virt_x) {
return first;
}
return last;
}
void sl_output_calculate_virtual_dimensions(struct sl_host_output* host) {
int scale;
int virt_physical_width;
int virt_physical_height;
int virt_width;
int virt_height;
if (host->ctx->use_direct_scale) {
sl_output_init_dimensions_direct(host, &scale, &virt_physical_width,
&virt_physical_height, &virt_width,
&virt_height);
} else {
sl_output_get_dimensions_original(host, &scale, &virt_physical_width,
&virt_physical_height, &virt_width,
&virt_height);
}
host->scale_factor = scale;
host->virt_width = virt_width;
host->virt_height = virt_height;
host->virt_physical_width = virt_physical_width;
host->virt_physical_height = virt_physical_height;
sl_output_apply_rotation(host->transform, virt_width, virt_height,
&host->virt_rotated_width,
&host->virt_rotated_height);
host->needs_update = true;
}
// Function which pushes the state of an output to the client.
void sl_output_send_host_output_state(struct sl_host_output* host) {
// Could be more granular, but the current implementation means that if one
// value changes, everything should be impacted.
if (host->needs_update) {
wl_output_send_geometry(host->resource, host->virt_x, 0,
host->virt_physical_width,
host->virt_physical_height, host->subpixel,
host->make, host->model, host->transform);
wl_output_send_mode(host->resource, host->flags | WL_OUTPUT_MODE_CURRENT,
host->virt_width, host->virt_height, host->refresh);
if (wl_resource_get_version(host->resource) >=
WL_OUTPUT_SCALE_SINCE_VERSION)
wl_output_send_scale(host->resource, host->scale_factor);
if (wl_resource_get_version(host->resource) >= WL_OUTPUT_DONE_SINCE_VERSION)
wl_output_send_done(host->resource);
host->needs_update = false;
}
}
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) {
void* result = wl_output_get_user_data(output);
sl_host_output* host = static_cast<sl_host_output*>(result);
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;
auto pointer = std::find(host->ctx->host_outputs.begin(),
host->ctx->host_outputs.end(), host);
assert(pointer != host->ctx->host_outputs.end());
// host_outputs is sorted by x. Delete then re-insert at the correct
// position.
host->ctx->host_outputs.erase(pointer);
// 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.
auto insert_at = host->ctx->host_outputs.end();
for (auto it = host->ctx->host_outputs.begin();
it != host->ctx->host_outputs.end(); ++it) {
if ((*it)->x > host->x) {
insert_at = it;
break;
}
}
host->ctx->host_outputs.insert(insert_at, host);
}
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<sl_host_output*>(wl_output_get_user_data(output));
host->flags = flags;
host->width = width;
host->height = height;
host->refresh = refresh;
host->needs_update = true;
}
static void sl_output_done(void* data, struct wl_output* output) {
struct sl_host_output* host =
static_cast<sl_host_output*>(wl_output_get_user_data(output));
// Early out if scale is expected but not yet know.
if (host->expecting_scale)
return;
// Recalculate according to any information that's been modified.
sl_output_calculate_virtual_dimensions(host);
// Shift all outputs that are to the right of host to the right if needed.
sl_output_update_output_x(host->ctx);
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<sl_host_output*>(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<sl_host_output*>(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<sl_host_output*>(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<sl_host_output*>(zaura_output_get_user_data(output));
host->device_scale_factor = device_scale_factor;
}
static const struct zaura_output_listener sl_aura_output_listener = {
sl_aura_output_scale, sl_aura_output_connection,
sl_aura_output_device_scale_factor, /*insets=*/DoNothing,
/*logical_transform=*/DoNothing};
static void sl_destroy_host_output(struct wl_resource* resource) {
struct sl_host_output* host =
static_cast<sl_host_output*>(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, nullptr);
auto pointer = std::find(host->ctx->host_outputs.begin(),
host->ctx->host_outputs.end(), host);
assert(pointer != host->ctx->host_outputs.end());
host->ctx->host_outputs.erase(pointer);
free(host->make);
free(host->model);
// Shift all outputs to the right of the deleted output to the left.
sl_output_update_output_x(host->ctx);
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<sl_host_output*>(
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<sl_host_output*>(
zxdg_output_v1_get_user_data(zxdg_output_v1));
host->logical_width = width;
host->logical_height = height;
host->expecting_logical_size = false;
}
static const struct zxdg_output_v1_listener sl_xdg_output_listener = {
sl_xdg_output_logical_position, sl_xdg_output_logical_size,
/*done=*/DoNothing,
/*name=*/DoNothing, /*desc=*/DoNothing};
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, nullptr, host,
sl_destroy_host_output);
host->proxy = static_cast<wl_output*>(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 = nullptr;
// We assume that first output is internal by default.
host->internal = ctx->host_outputs.empty();
// We'll always need to forward this information.
host->needs_update = true;
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->virt_physical_width = 0;
host->virt_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->virt_width = 1024;
host->virt_height = 768;
host->virt_rotated_width = 0;
host->virt_rotated_height = 0;
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;
ctx->host_outputs.push_back(host);
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);
}