blob: aef8208d229e949f7f29795c5c19e6e58b637b52 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "display-plan.h"
#include "display-priv.h"
#include "guacamole/assert.h"
#include "guacamole/client.h"
#include "guacamole/display.h"
#include "guacamole/fifo.h"
#include "guacamole/flag.h"
#include "guacamole/mem.h"
#include "guacamole/protocol.h"
#include "guacamole/rect.h"
#include "guacamole/rwlock.h"
#include "guacamole/user.h"
#include <string.h>
/**
* Begins a section related to an optimization phase that should be tracked for
* performance at the "trace" log level.
*/
#define GUAC_DISPLAY_PLAN_BEGIN_PHASE() \
do { \
guac_timestamp phase_start = guac_timestamp_current();
/**
* Ends a section related to an optimization phase that should be tracked for
* performance at the "trace" log level.
*
* @param display
* The guac_display related to the optimizations being performed.
*
* @param phase
* A human-readable name for the optimization phase being tracked.
*
* @param n
* The ordinal number of this phase relative to other phases, where the
* first phase is phase 1.
*
* @param total
* The total number of optimization phases.
*/
#define GUAC_DISPLAY_PLAN_END_PHASE(display, phase, n, total) \
guac_timestamp phase_end = guac_timestamp_current(); \
guac_client_log(display->client, GUAC_LOG_TRACE, "Render planning " \
"phase %i/%i (%s): %ims", n, total, phase, \
(int) (phase_end - phase_start)); \
} while (0)
void guac_display_end_frame(guac_display* display) {
guac_display_end_multiple_frames(display, 0);
}
/**
* Callback for guac_client_foreach_user() which sends the current cursor
* position and button state to any given user except the user that moved the
* cursor last.
*
* @param data
* A pointer to the guac_display whose cursor state should be broadcast to
* all users except the user that moved the cursor last.
*
* @return
* Always NULL.
*/
static void* LFR_guac_display_broadcast_cursor_state(guac_user* user, void* data) {
guac_display* display = (guac_display*) data;
/* Send cursor state only if the user is not moving the cursor */
if (user != display->last_frame.cursor_user) {
guac_protocol_send_mouse(user->socket,
display->last_frame.cursor_x, display->last_frame.cursor_y,
display->last_frame.cursor_mask, display->last_frame.timestamp);
guac_socket_flush(user->socket);
}
return NULL;
}
/**
* Finalizes the current pending frame, storing that state as the copy of the
* last frame. All layer properties that have changed since the last frame will
* be sent out to connected clients.
*
* @param display
* The display whose pending frame should be finalized and persisted as the
* last frame.
*
* @return
* Non-zero if any layers within the pending frame had any changes
* whatsoever that needed to be sent as part of the frame, zero otherwise.
*/
static int PFW_LFW_guac_display_frame_complete(guac_display* display) {
guac_client* client = display->client;
int retval = 0;
display->last_frame.layers = display->pending_frame.layers;
for (guac_display_layer* current = display->pending_frame.layers;
current != NULL; current = current->pending_frame.next) {
/* Duplicate layers from pending frame to last frame */
current->last_frame.prev = current->pending_frame.prev;
current->last_frame.next = current->pending_frame.next;
/* Skip non-existential updates for any layer whose buffer has been
* replaced with NULL (this is intentionally allowed to ensure references
* to external buffers can be safely removed if necessary, even before
* guac_display is freed) */
if (current->pending_frame.buffer == NULL) {
GUAC_ASSERT(current->pending_frame.buffer_is_external);
current->last_frame.dirty = (guac_rect) { 0 };
continue;
}
/* Always resize the last_frame buffer to match the pending_frame prior
* to copying over any changes (this is particularly important given
* that the pending_frame buffer can be replaced with an external
* buffer). Since this involves copying over all data from the
* pending frame, we can skip the later pending frame copy based on
* whether the pending frame is dirty. */
if (current->last_frame.buffer_stride != current->pending_frame.buffer_stride
|| current->last_frame.buffer_width != current->pending_frame.buffer_width
|| current->last_frame.buffer_height != current->pending_frame.buffer_height) {
size_t buffer_size = guac_mem_ckd_mul_or_die(current->pending_frame.buffer_height,
current->pending_frame.buffer_stride);
guac_mem_free(current->last_frame.buffer);
current->last_frame.buffer = guac_mem_zalloc(buffer_size);
memcpy(current->last_frame.buffer, current->pending_frame.buffer, buffer_size);
current->last_frame.buffer_stride = current->pending_frame.buffer_stride;
current->last_frame.buffer_width = current->pending_frame.buffer_width;
current->last_frame.buffer_height = current->pending_frame.buffer_height;
current->last_frame.dirty = current->pending_frame.dirty;
current->pending_frame.dirty = (guac_rect) { 0 };
retval = 1;
}
/* Copy over pending frame contents if actually changed (this is not
* necessary if the last_frame buffer was resized to match
* pending_frame, as a copy from pending_frame to last_frame is
* inherently part of that) */
else if (!guac_rect_is_empty(&current->pending_frame.dirty)) {
unsigned char* pending_frame = current->pending_frame.buffer;
unsigned char* last_frame = current->last_frame.buffer;
size_t row_length = guac_mem_ckd_mul_or_die(current->pending_frame.width, 4);
for (int y = 0; y < current->pending_frame.height; y++) {
memcpy(last_frame, pending_frame, row_length);
last_frame += current->last_frame.buffer_stride;
pending_frame += current->pending_frame.buffer_stride;
}
current->last_frame.dirty = current->pending_frame.dirty;
current->pending_frame.dirty = (guac_rect) { 0 };
retval = 1;
}
/* Even if nothing has changed in the pending frame, we have to at
* least flush that fact to the last frame (otherwise, the last frame
* may contain stale dirty rects) */
else
current->last_frame.dirty = (guac_rect) { 0 };
/* Commit any change in layer size */
if (current->pending_frame.width != current->last_frame.width
|| current->pending_frame.height != current->last_frame.height) {
guac_protocol_send_size(client->socket, current->layer,
current->pending_frame.width, current->pending_frame.height);
current->last_frame.width = current->pending_frame.width;
current->last_frame.height = current->pending_frame.height;
retval = 1;
}
/* Commit any change in layer opacity */
if (current->pending_frame.opacity != current->last_frame.opacity) {
guac_protocol_send_shade(client->socket, current->layer,
current->pending_frame.opacity);
current->last_frame.opacity = current->pending_frame.opacity;
retval = 1;
}
/* Commit any change in layer location/hierarchy */
if (current->pending_frame.x != current->last_frame.x
|| current->pending_frame.y != current->last_frame.y
|| current->pending_frame.z != current->last_frame.z
|| current->pending_frame.parent != current->last_frame.parent) {
guac_protocol_send_move(client->socket, current->layer,
current->pending_frame.parent,
current->pending_frame.x,
current->pending_frame.y,
current->pending_frame.z);
current->last_frame.x = current->pending_frame.x;
current->last_frame.y = current->pending_frame.y;
current->last_frame.z = current->pending_frame.z;
current->last_frame.parent = current->pending_frame.parent;
retval = 1;
}
/* Commit any change in layer multitouch support */
if (current->pending_frame.touches != current->last_frame.touches) {
guac_protocol_send_set_int(client->socket, current->layer,
GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH,
current->pending_frame.touches);
current->last_frame.touches = current->pending_frame.touches;
}
/* Commit any hinting regarding scroll/copy optimization (NOTE: While
* this value is copied for consistency, it will already have taken
* effect in the context of the pending frame due to the scroll/copy
* optimization pass having occurred prior to the call to this
* function) */
current->last_frame.search_for_copies = current->pending_frame.search_for_copies;
current->pending_frame.search_for_copies = 0;
/* Commit any change in lossless setting (no need to synchronize this
* to the client - it affects only how last_frame is interpreted) */
current->last_frame.lossless = current->pending_frame.lossless;
}
display->last_frame.timestamp = display->pending_frame.timestamp;
display->last_frame.frames = display->pending_frame.frames;
display->pending_frame.frames = 0;
display->pending_frame_dirty_excluding_mouse = 0;
/* Commit cursor hotspot */
display->last_frame.cursor_hotspot_x = display->pending_frame.cursor_hotspot_x;
display->last_frame.cursor_hotspot_y = display->pending_frame.cursor_hotspot_y;
/* Commit mouse cursor location and notify all other users of change in
* cursor state */
if (display->pending_frame.cursor_x != display->last_frame.cursor_x
|| display->pending_frame.cursor_y != display->last_frame.cursor_y
|| display->pending_frame.cursor_mask != display->last_frame.cursor_mask) {
display->last_frame.cursor_user = display->pending_frame.cursor_user;
display->last_frame.cursor_x = display->pending_frame.cursor_x;
display->last_frame.cursor_y = display->pending_frame.cursor_y;
display->last_frame.cursor_mask = display->pending_frame.cursor_mask;
guac_client_foreach_user(client, LFR_guac_display_broadcast_cursor_state, display);
/* NOTE: We DO NOT set retval here, as flushing a frame due purely to
* mouse position changes can cause slowdowns apparently from the sheer
* quantity of frames */
}
return retval;
}
void guac_display_end_mouse_frame(guac_display* display) {
guac_rwlock_acquire_write_lock(&display->pending_frame.lock);
if (!display->pending_frame_dirty_excluding_mouse)
guac_display_end_multiple_frames(display, 0);
guac_rwlock_release_lock(&display->pending_frame.lock);
}
void guac_display_end_multiple_frames(guac_display* display, int frames) {
guac_display_plan* plan = NULL;
guac_display_layer* removed_layers = NULL;
guac_rwlock_acquire_write_lock(&display->pending_frame.lock);
display->pending_frame.frames += frames;
/* Defer rendering of further frames until after any in-progress frame has
* finished. Graphical changes will meanwhile continue being accumulated in
* the pending frame. */
guac_fifo_lock(&display->ops);
int defer_frame = display->frame_deferred =
(display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY) || display->active_workers;
guac_fifo_unlock(&display->ops);
if (defer_frame)
goto finished_with_pending_frame_lock;
guac_rwlock_acquire_write_lock(&display->last_frame.lock);
/* PASS 0: Create naive plan, identify minimal dirty rects by comparing the
* changes between the pending and last frames.
*
* This plan will contain operations covering only the minimal parts of the
* display that have changed, but is naive in the sense that it only
* produces draw operations covering 64x64 cells. There is room for
* optimization of those operations, which will be performed by further
* passes. */
GUAC_DISPLAY_PLAN_BEGIN_PHASE();
plan = PFW_LFR_guac_display_plan_create(display);
GUAC_DISPLAY_PLAN_END_PHASE(display, "draft", 1, 5);
if (plan != NULL) {
display->pending_frame.timestamp = plan->frame_end;
/* PASS 1: Identify draw operations that only apply a single color, and
* replace those operations with simple rectangle draws. */
GUAC_DISPLAY_PLAN_BEGIN_PHASE();
PFR_guac_display_plan_rewrite_as_rects(plan);
GUAC_DISPLAY_PLAN_END_PHASE(display, "rects", 2, 5);
/* PASS 2 (and 3): Index all modified cells by their graphical contents and
* search the previous frame for occurrences of the same content. Where any
* draws could instead be represented as copies from the previous frame, do
* so instead of sending new image data. */
GUAC_DISPLAY_PLAN_BEGIN_PHASE();
PFR_guac_display_plan_index_dirty_cells(plan);
PFR_LFR_guac_display_plan_rewrite_as_copies(plan);
GUAC_DISPLAY_PLAN_END_PHASE(display, "search", 3, 5);
/* PASS 4 (and 5): Combine adjacent updates in horizontal and vertical
* directions where doing so would be more efficient. The goal of these
* passes is to ensure that graphics can be encoded and decoded
* efficiently, without defeating the parralelism provided by providing the
* worker threads with many smaller operations. */
GUAC_DISPLAY_PLAN_BEGIN_PHASE();
PFW_guac_display_plan_combine_horizontally(plan);
PFW_guac_display_plan_combine_vertically(plan);
GUAC_DISPLAY_PLAN_END_PHASE(display, "combine", 4, 5);
}
/*
* With all optimizations now performed, finalize the pending frame. This
* sets the worker threads in motion and frees up the pending frame
* surfaces for writing. Drawing to the next pending frame can now occur
* without disturbing the encoding performed by the worker threads.
*/
int frame_nonempty;
GUAC_DISPLAY_PLAN_BEGIN_PHASE();
frame_nonempty = PFW_LFW_guac_display_frame_complete(display);
GUAC_DISPLAY_PLAN_END_PHASE(display, "commit", 5, 5);
guac_rwlock_release_lock(&display->last_frame.lock);
/* The last frame layer list has now been rebuilt from the pending frame
* layer list, so any layers removed since the previous flush are no longer
* part of either frame. Detach them for destruction below.
*
* IMPORTANT: These layers MUST NOT be freed until after the frame is fully
* flushed. The display plan may reference a removed layer's buffer as the
* source of a copy operation, and that plan has not yet been applied. */
removed_layers = display->pending_frame_removed_layers;
display->pending_frame_removed_layers = NULL;
/* Awaken worker threads to perform the rest of the tasks required for the
* frame (if any such tasks remain) */
if (plan != NULL) {
guac_display_plan_apply(plan);
guac_display_plan_free(plan);
}
/* Not all frames are graphical, and not all frames result in operations
* reaching the worker threads. If we end up with a frame containing
* nothing but layer property changes or nothing but non-image updates,
* then we must still send at least one operation to awaken the workers,
* flush any layer changes, and mark the end of the frame with a "sync",
* even though there is no display plan to optimize. */
if (frame_nonempty) {
guac_display_plan_operation end_frame_op = {
.type = GUAC_DISPLAY_PLAN_OPERATION_NOP
};
guac_fifo_enqueue(&display->ops, &end_frame_op);
}
finished_with_pending_frame_lock:
guac_rwlock_release_lock(&display->pending_frame.lock);
/* Free any layers detached above. NOTE: This is intentionally done outside
* the pending frame lock to avoid contending with drawing operations. */
guac_display_free_removed_layers(display, removed_layers);
}