diff --git a/include/common/scaled_scene_buffer.h b/include/common/scaled_scene_buffer.h new file mode 100644 index 00000000..fa2cb91f --- /dev/null +++ b/include/common/scaled_scene_buffer.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __LAB_COMMON_SCALED_SCENE_BUFFER_H +#define __LAB_COMMON_SCALED_SCENE_BUFFER_H + +#define LAB_SCALED_BUFFER_MAX_CACHE 2 + +struct wl_list; +struct wlr_buffer; +struct wl_listener; +struct wlr_scene_tree; +struct lab_data_buffer; +struct scaled_scene_buffer; + +struct scaled_scene_buffer_impl { + /* Return a new buffer optimized for the new scale */ + struct lab_data_buffer *(*create_buffer) + (struct scaled_scene_buffer *scaled_buffer, double scale); + /* Might be NULL or used for cleaning up */ + void (*destroy)(struct scaled_scene_buffer *scaled_buffer); +}; + +struct scaled_scene_buffer { + struct wlr_scene_buffer *scene_buffer; + int width; /* unscaled, read only */ + int height; /* unscaled, read only */ + void *data; /* opaque user data */ + + /* Private */ + double active_scale; + struct wl_list cache; /* struct scaled_buffer_cache_entry.link */ + struct wl_listener destroy; + struct wl_listener output_enter; + struct wl_listener output_leave; + const struct scaled_scene_buffer_impl *impl; +}; + +/** + * Create an auto scaling buffer that creates a wlr_scene_buffer + * and subscribes to its output_enter and output_leave signals. + * + * If the maximal scale changes, it either sets an already existing buffer + * that was rendered for the current scale or - if there is none - calls + * implementation->create_buffer(self, scale) to get a new lab_data_buffer + * optimized for the new scale. + * + * Up to LAB_SCALED_BUFFER_MAX_CACHE (2) buffers are cached in an LRU fashion + * to handle the majority of use cases where a view is moved between no more + * than two different scales. + * + * scaled_scene_buffer will clean up automatically once the internal + * wlr_scene_buffer is being destroyed. If implementation->destroy is set + * it will also get called so a consumer of this API may clean up its own + * allocations. + */ +struct scaled_scene_buffer *scaled_scene_buffer_create( + struct wlr_scene_tree *parent, + const struct scaled_scene_buffer_impl *implementation); + +/* Clear the cache of existing buffers, useful in case the content changes */ +void scaled_scene_buffer_invalidate_cache(struct scaled_scene_buffer *self); + + +/* Private */ +struct scaled_scene_buffer_cache_entry { + struct wl_list link; /* struct scaled_scene_buffer.cache */ + struct wlr_buffer *buffer; + double scale; +}; + +#endif /* __LAB_COMMON_SCALED_SCENE_BUFFER_H */ diff --git a/src/common/meson.build b/src/common/meson.build index fdfa72b2..2e4407c8 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -4,6 +4,7 @@ labwc_sources += files( 'font.c', 'grab-file.c', 'nodename.c', + 'scaled_scene_buffer.c', 'scene-helpers.c', 'spawn.c', 'string-helpers.c', diff --git a/src/common/scaled_scene_buffer.c b/src/common/scaled_scene_buffer.c new file mode 100644 index 00000000..95e2673d --- /dev/null +++ b/src/common/scaled_scene_buffer.c @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include "buffer.h" +#include "common/scaled_scene_buffer.h" + +/** + * TODO + * + * This implementation does not react to output scale changes itself but only + * to movement of scene nodes to different outputs. To also handle output scale + * changes we'd need to keep a list of struct wlr_outputs * in sync and listen + * to their commit signals. + * + * The detection of the max output scale is also not 100% robust for all use + * cases as on output_enter we only compare the new output scale with the + * primary_output scale. In specific conditions the primary output may be the + * same as the new output even though a smaller part of the buffer node is + * still visible on an output with a bigger scale. This could be solved the + * same way as the previous point in keeping a list of outputs in sync. + * + * Most of this would be easier when wlroots would instead provide a + * max_scale_changed event because it already touches all the relevant parts + * when calculating the primary_output and inform wlr_scene_buffers about + * output changes. + * + * See wlroots/types/scene/wlr_scene.c scene_buffer_update_outputs() + */ + +/* Internal API */ +static void +_cache_entry_destroy(struct scaled_scene_buffer_cache_entry *cache_entry) +{ + wl_list_remove(&cache_entry->link); + if (cache_entry->buffer) { + wlr_buffer_drop(cache_entry->buffer); + } + free(cache_entry); +} + +static void +_update_buffer(struct scaled_scene_buffer *self, double scale) +{ + self->active_scale = scale; + + /* Search for cached buffer of specified scale */ + struct scaled_scene_buffer_cache_entry *cache_entry, *cache_entry_tmp; + wl_list_for_each_safe(cache_entry, cache_entry_tmp, &self->cache, link) { + if (cache_entry->scale == scale) { + /* LRU cache, recently used in front */ + wl_list_remove(&cache_entry->link); + wl_list_insert(&self->cache, &cache_entry->link); + wlr_scene_buffer_set_buffer(self->scene_buffer, cache_entry->buffer); + return; + } + } + + /* Create new buffer, will get destroyed along the backing wlr_buffer */ + struct lab_data_buffer *buffer = self->impl->create_buffer(self, scale); + self->width = buffer ? buffer->unscaled_width : 0; + self->height = buffer ? buffer->unscaled_height : 0; + + /* Create or reuse cache entry */ + if (wl_list_length(&self->cache) < LAB_SCALED_BUFFER_MAX_CACHE) { + cache_entry = calloc(1, sizeof(*cache_entry)); + } else { + cache_entry = wl_container_of(self->cache.prev, cache_entry, link); + if (cache_entry->buffer) { + wlr_buffer_drop(cache_entry->buffer); + } + wl_list_remove(&cache_entry->link); + } + + /* Update the cache entry */ + cache_entry->scale = scale; + cache_entry->buffer = buffer ? &buffer->base : NULL; + wl_list_insert(&self->cache, &cache_entry->link); + + /* And finally update the wlr_scene_buffer itself */ + wlr_scene_buffer_set_buffer(self->scene_buffer, cache_entry->buffer); + wlr_scene_buffer_set_dest_size(self->scene_buffer, self->width, self->height); +} + +/* Internal event handlers */ +static void +_handle_node_destroy(struct wl_listener *listener, void *data) +{ + struct scaled_scene_buffer_cache_entry *cache_entry, *cache_entry_tmp; + struct scaled_scene_buffer *self = wl_container_of(listener, self, destroy); + + wl_list_remove(&self->destroy.link); + wl_list_remove(&self->output_enter.link); + wl_list_remove(&self->output_leave.link); + + wl_list_for_each_safe(cache_entry, cache_entry_tmp, &self->cache, link) { + _cache_entry_destroy(cache_entry); + } + assert(wl_list_empty(&self->cache)); + + if (self->impl->destroy) { + self->impl->destroy(self); + } + free(self); +} + +static void +_handle_output_enter(struct wl_listener *listener, void *data) +{ + struct scaled_scene_buffer *self + = wl_container_of(listener, self, output_enter); + /* primary_output is the output most of the node area is in */ + struct wlr_scene_output *primary = self->scene_buffer->primary_output; + /* scene_output is the output we just entered */ + struct wlr_scene_output *scene_output = data; + double max_scale = scene_output->output->scale; + + if (primary && primary->output->scale > max_scale) { + max_scale = primary->output->scale; + } + + if (self->active_scale != max_scale) { + _update_buffer(self, max_scale); + } +} + +static void +_handle_output_leave(struct wl_listener *listener, void *data) +{ + struct scaled_scene_buffer *self + = wl_container_of(listener, self, output_leave); + /* primary_output is the output most of the node area is in */ + struct wlr_scene_output *primary = self->scene_buffer->primary_output; + + if (primary && primary->output->scale != self->active_scale) { + _update_buffer(self, primary->output->scale); + } +} + +/* Public API */ +struct scaled_scene_buffer * +scaled_scene_buffer_create(struct wlr_scene_tree *parent, + const struct scaled_scene_buffer_impl *impl) +{ + assert(parent); + assert(impl); + assert(impl->create_buffer); + + struct scaled_scene_buffer *self = calloc(1, sizeof(*self)); + if (!self) { + return NULL; + } + + self->scene_buffer = wlr_scene_buffer_create(parent, NULL); + if (!self->scene_buffer) { + wlr_log(WLR_ERROR, "Failed to create scene buffer"); + free(self); + return NULL; + } + + self->impl = impl; + self->active_scale = 1; + wl_list_init(&self->cache); + + /* Listen to output enter/leave so we get notified about scale changes */ + self->output_enter.notify = _handle_output_enter; + wl_signal_add(&self->scene_buffer->events.output_enter, &self->output_enter); + self->output_leave.notify = _handle_output_leave; + wl_signal_add(&self->scene_buffer->events.output_leave, &self->output_leave); + + /* Let it destroy automatically when the scene node destroys */ + self->destroy.notify = _handle_node_destroy; + wl_signal_add(&self->scene_buffer->node.events.destroy, &self->destroy); + + return self; +} + +void +scaled_scene_buffer_invalidate_cache(struct scaled_scene_buffer *self) +{ + assert(self); + struct scaled_scene_buffer_cache_entry *cache_entry, *cache_entry_tmp; + wl_list_for_each_safe(cache_entry, cache_entry_tmp, &self->cache, link) { + _cache_entry_destroy(cache_entry); + } + assert(wl_list_empty(&self->cache)); + _update_buffer(self, self->active_scale); +}