blob: 7f500cf36dc98830fcee0ed9c3a3643f5f388a97 [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/client.h"
#include "guacamole/display.h"
#include "guacamole/fifo.h"
#include "guacamole/layer.h"
#include "guacamole/proctitle.h"
#include "guacamole/protocol-types.h"
#include "guacamole/protocol.h"
#include "guacamole/rect.h"
#include "guacamole/rwlock.h"
#include "guacamole/socket.h"
#include "guacamole/timestamp.h"
#include <inttypes.h>
#include <limits.h>
#include <cairo/cairo.h>
#include <pthread.h>
/**
* Returns a new Cairo surface representing the contents of the given dirty
* rectangle from the given layer. The returned surface must eventually be
* freed with a call to cairo_surface_destroy(). The graphical contents will be
* referenced from the layer's last_frame buffer. If sending the contents of a
* pending frame, that pending frame must have been copied over to the
* last_frame buffer before calling this function.
*
* @param display_layer
* The layer whose data should be referenced by the returned Cairo surface.
*
* @param dirty
* The region of the layer that should be referenced by the returned Cairo
* surface.
*
* @return
* A new Cairo surface that points to the given rectangle of image data
* from the last_frame buffer of the given layer. This surface must
* eventually be freed with a call to cairo_surface_destroy().
*/
static cairo_surface_t* LFR_guac_display_layer_cairo_rect(guac_display_layer* display_layer,
guac_rect* dirty) {
/* Get Cairo surface covering dirty rect */
unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty);
cairo_surface_t* rect;
/* Use RGB24 if the image is fully opaque */
if (display_layer->opaque)
rect = cairo_image_surface_create_for_data(buffer,
CAIRO_FORMAT_RGB24, guac_rect_width(dirty),
guac_rect_height(dirty), display_layer->last_frame.buffer_stride);
/* Otherwise ARGB32 is needed, and the destination must be cleared */
else
rect = cairo_image_surface_create_for_data(buffer,
CAIRO_FORMAT_ARGB32, guac_rect_width(dirty),
guac_rect_height(dirty), display_layer->last_frame.buffer_stride);
return rect;
}
/**
* Sends instructions over the Guacamole connection to clear the given
* rectangle of the given layer if that layer is non-opaque. This is necessary
* prior to sending image data to layers with alpha transparency, as image data
* from multiple updates will otherwise be composited together.
*
* @param display_layer
* The layer that should possibly be cleared in preparation for a future
* drawing operation.
*
* @param dirty
* The rectangular region of the drawing operation.
*/
static void guac_display_layer_clear_non_opaque(guac_display_layer* display_layer,
guac_rect* dirty) {
guac_display* display = display_layer->display;
const guac_layer* layer = display_layer->layer;
guac_client* client = display->client;
guac_socket* socket = client->socket;
/* Clear destination region only if necessary due to the relevant layer
* being non-opaque. The rect+cfill pair must be sent atomically to
* prevent interleaving with concurrent workers operating on the same
* layer. */
if (!display_layer->opaque) {
pthread_mutex_lock(&display_layer->path_lock);
guac_protocol_send_rect(socket, layer, dirty->left, dirty->top,
guac_rect_width(dirty), guac_rect_height(dirty));
guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer,
0x00, 0x00, 0x00, 0xFF);
pthread_mutex_unlock(&display_layer->path_lock);
}
}
/**
* Returns an appropriate quality between 0 and 100 for lossy encoding
* depending on the current processing lag calculated for the given client.
*
* @param client
* The client for which the lossy quality is being calculated.
*
* @return
* A value between 0 and 100 inclusive which seems appropriate for the
* client based on lag measurements.
*/
static int guac_display_suggest_quality(guac_client* client) {
int lag = guac_client_get_processing_lag(client);
/* Scale quality linearly from 90 to 30 as lag varies from 20ms to 80ms */
int quality = 90 - (lag - 20);
/* Do not exceed 90 for quality */
if (quality > 90)
return 90;
/* Do not go below 30 for quality */
if (quality < 30)
return 30;
return quality;
}
/**
* Guesses whether a rectangle within a particular layer would be better
* compressed as PNG or using a lossy format like JPEG. Positive values
* indicate PNG is likely to be superior, while negative values indicate the
* opposite.
*
* @param layer
* The layer containing the image data to check.
*
* @param rect
* The rect to check within the given layer.
*
* @return
* Positive values if PNG compression is likely to perform better than
* lossy alternatives, or negative values if PNG is likely to perform
* worse.
*/
static int LFR_guac_display_layer_png_optimality(guac_display_layer* layer,
const guac_rect* rect) {
int x, y;
int num_same = 0;
int num_different = 1;
/* Get buffer from layer */
size_t stride = layer->last_frame.buffer_stride;
const unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(layer->last_frame, *rect);
/* Image must be at least 1x1 */
if (rect->right - rect->left < 1 || rect->bottom - rect->top< 1)
return 0;
/* For each row */
for (y = rect->top; y < rect->bottom; y++) {
uint32_t* row = (uint32_t*) buffer;
uint32_t last_pixel = *(row++) | 0xFF000000;
/* For each pixel in current row */
for (x = rect->left + 1; x < rect->right; x++) {
/* Get next pixel */
uint32_t current_pixel = *(row++) | 0xFF000000;
/* Update same/different counts according to pixel value */
if (current_pixel == last_pixel)
num_same++;
else
num_different++;
last_pixel = current_pixel;
}
/* Advance to next row */
buffer += stride;
}
/* Return rough approximation of optimality for PNG compression. As PNG
* leverages lossless DEFLATE compression (which works by reducing the
* number of bytes required to represent repeated data), an approximation
* of the amount of repeated image data within the image is a reasonable
* approximation for how well an image will compress. */
return 0x100 * num_same / num_different - 0x400;
}
/**
* Returns whether the given rectangle would be optimally encoded as JPEG
* rather than PNG.
*
* @param layer
* The layer to be queried.
*
* @param rect
* The rectangle to check.
*
* @param framerate
* The rate that the region covered by the given rectangle has historically
* been being updated within the given layer, in frames per second.
*
* @return
* Non-zero if the rectangle would be optimally encoded as JPEG, zero
* otherwise.
*/
static int LFR_guac_display_layer_should_use_jpeg(guac_display_layer* layer,
const guac_rect* rect, int framerate) {
/* Do not use JPEG if lossless quality is required */
if (layer->last_frame.lossless)
return 0;
int rect_width = rect->right - rect->left;
int rect_height = rect->bottom - rect->top;
int rect_size = rect_width * rect_height;
/* JPEG is preferred if:
* - frame rate is high enough
* - image size is large enough
* - PNG is not more optimal based on image contents */
return framerate >= GUAC_DISPLAY_JPEG_FRAMERATE
&& rect_size > GUAC_DISPLAY_JPEG_MIN_BITMAP_SIZE
&& LFR_guac_display_layer_png_optimality(layer, rect) < 0;
}
/**
* Returns whether the given rectangle would be optimally encoded as WebP
* rather than PNG.
*
* @param layer
* The layer to be queried.
*
* @param rect
* The rectangle to check.
*
* @param framerate
* The rate that the region covered by the given rectangle has historically
* been being updated within the given layer, in frames per second.
*
* @return
* Non-zero if the rectangle would be optimally encoded as WebP, zero
* otherwise.
*/
static int LFR_guac_display_layer_should_use_webp(guac_display_layer* layer,
const guac_rect* rect, int framerate) {
/* Do not use WebP if not supported */
if (!guac_client_supports_webp(layer->display->client))
return 0;
/* WebP is preferred if:
* - frame rate is high enough
* - PNG is not more optimal based on image contents */
return framerate >= GUAC_DISPLAY_JPEG_FRAMERATE
&& LFR_guac_display_layer_png_optimality(layer, rect) < 0;
}
void* guac_display_worker_thread(void* data) {
/* Thread name display-wrk: one worker in the display pool; encodes and
* sends graphical updates for dirty layer regions. */
guac_thread_name_set("display-wrk");
int framerate;
int has_outstanding_frames = 0;
guac_display* display = (guac_display*) data;
guac_client* client = display->client;
guac_socket* socket = client->socket;
guac_display_plan_operation op;
while (guac_fifo_dequeue_and_lock(&display->ops, &op)) {
/* Notify any watchers of render_state that a frame is now in progress */
guac_flag_set_and_lock(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS);
guac_flag_clear(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS);
guac_flag_unlock(&display->render_state);
/* NOTE: Any thread that locks the operation queue can know that there
* are no pending operations in progress if the queue is empty and
* there are no active workers */
display->active_workers++;
guac_fifo_unlock(&display->ops);
guac_rwlock_acquire_read_lock(&display->last_frame.lock);
guac_display_layer* display_layer = op.layer;
switch (op.type) {
case GUAC_DISPLAY_PLAN_OPERATION_IMG:
framerate = INT_MAX;
if (op.current_frame > op.last_frame)
framerate = 1000 / (op.current_frame - op.last_frame);
guac_rect* dirty = &op.dest;
/* TODO: Determine whether to use PNG/WebP/JPEG purely
* based on whether lossless encoding is required, the
* expected time until another frame is received (time
* since last frame), and estimated encoding times. The
* time allowed per update should be divided up
* proportionately based on the dirty_size of the update. */
/* TODO: Stream PNG/WebP/JPEG using progressive encoding such
* that a frame that is currently being encoded can be
* preempted by the next frame, with the connected client then
* simply receiving a lower-quality intermediate frame. If
* necessary, progressive encoding can be achieved by manually
* dividing images into multiple reduced-resolution stages,
* such that each image streamed is actually only one quarter
* the size of the original image. Compositing via Guacamole
* protocol instructions can reassemble those stages. */
cairo_surface_t* rect = LFR_guac_display_layer_cairo_rect(display_layer, dirty);
const guac_layer* layer = display_layer->layer;
/* Clear relevant rect of destination layer if necessary to
* ensure fresh data is not drawn on top of old data for layers
* with alpha transparency */
guac_display_layer_clear_non_opaque(display_layer, dirty);
/* Prefer WebP when reasonable */
if (LFR_guac_display_layer_should_use_webp(display_layer, dirty, framerate))
guac_client_stream_webp(client, socket, GUAC_COMP_OVER, layer,
dirty->left, dirty->top, rect,
guac_display_suggest_quality(client),
display_layer->last_frame.lossless ? 1 : 0);
/* If not WebP, JPEG is the next best (lossy) choice */
else if (display_layer->opaque && LFR_guac_display_layer_should_use_jpeg(display_layer, dirty, framerate))
guac_client_stream_jpeg(client, socket, GUAC_COMP_OVER, layer,
dirty->left, dirty->top, rect,
guac_display_suggest_quality(client));
/* Use PNG if no lossy formats are appropriate */
else
guac_client_stream_png(client, socket, GUAC_COMP_OVER,
layer, dirty->left, dirty->top, rect);
cairo_surface_destroy(rect);
break;
case GUAC_DISPLAY_PLAN_OPERATION_COPY:
case GUAC_DISPLAY_PLAN_OPERATION_RECT:
guac_client_log(client, GUAC_LOG_DEBUG, "Operation type %i "
"should NOT be present in the set of operations given "
"to guac_display worker thread. All operations except "
"IMG and NOP are handled during the initial, "
"single-threaded flush step. This is likely a bug.",
op.type);
break;
case GUAC_DISPLAY_PLAN_OPERATION_NOP:
/* Do nothing */
break;
}
guac_fifo_lock(&display->ops);
/* If we're the only active worker and there are no further operations
* pending, we've reached the end of the frame, and this is the worker
* that will be sending that boundary to connected users */
if (!(display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY) && display->active_workers == 1) {
/* Update the mouse cursor if it's been changed since the
* last frame */
guac_display_layer* cursor = display->cursor_buffer;
if (!guac_rect_is_empty(&cursor->last_frame.dirty)) {
guac_protocol_send_cursor(client->socket,
display->last_frame.cursor_hotspot_x,
display->last_frame.cursor_hotspot_y,
cursor->layer, 0, 0,
cursor->last_frame.width,
cursor->last_frame.height);
}
/* Allow connected clients to move forward with rendering */
guac_client_end_multiple_frames(client, display->last_frame.frames);
/* While connected clients moves forward with rendering,
* commit any changed contents to client-side backing buffer */
guac_display_layer* current = display->last_frame.layers;
while (current != NULL) {
/* Save a copy of the changed region if the layer has
* been modified since the last frame */
guac_rect* dirty = &current->last_frame.dirty;
if (!guac_rect_is_empty(dirty)) {
int x = dirty->left;
int y = dirty->top;
int width = guac_rect_width(dirty);
int height = guac_rect_height(dirty);
/* Ensure destination region is cleared out first if the alpha channel need be considered,
* as GUAC_COMP_OVER is significantly faster than GUAC_COMP_SRC on the browser side */
if (!current->opaque) {
guac_protocol_send_rect(client->socket, current->last_frame_buffer, x, y, width, height);
guac_protocol_send_cfill(client->socket, GUAC_COMP_RATOP, current->last_frame_buffer,
0x00, 0x00, 0x00, 0x00);
}
guac_protocol_send_copy(client->socket,
current->layer, x, y, width, height,
GUAC_COMP_OVER, current->last_frame_buffer, x, y);
}
current = current->last_frame.next;
}
/* This is now absolutely everything for the current frame,
* and it's safe to flush any outstanding data */
guac_socket_flush(client->socket);
/* Notify any watchers of render_state that a frame is no longer in progress */
guac_flag_set_and_lock(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS);
guac_flag_clear(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS);
guac_flag_unlock(&display->render_state);
has_outstanding_frames = display->frame_deferred;
}
display->active_workers--;
guac_fifo_unlock(&display->ops);
guac_rwlock_release_lock(&display->last_frame.lock);
/* Trigger additional flush if frames were completed while we were
* still processing the previous frame */
if (has_outstanding_frames) {
guac_display_end_multiple_frames(display, 0);
has_outstanding_frames = 0;
}
}
return NULL;
}