diff --git a/.gitignore b/.gitignore index e72000d8e..d8b26f01f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ doc/*/doxygen-output # IDE metadata nbproject/ + +# Compilation database, as may be generated by tools like Bear +.cache/ +compile_commands.json diff --git a/configure.ac b/configure.ac index fb7612f34..a66f53626 100644 --- a/configure.ac +++ b/configure.ac @@ -44,9 +44,14 @@ PKG_PROG_PKG_CONFIG() AC_CHECK_HEADERS([fcntl.h stdlib.h string.h sys/socket.h time.h sys/time.h syslog.h unistd.h cairo/cairo.h pngstruct.h]) # Source characteristics +AC_DEFINE([_GNU_SOURCE], [1], [Uses GNU-specific APIs (if available)]) AC_DEFINE([_XOPEN_SOURCE], [700], [Uses X/Open and POSIX APIs]) AC_DEFINE([__BSD_VISIBLE], [1], [Uses BSD-specific APIs (if available)]) +# Check for availability of non-portable sched_getaffinity() function (one of +# several possible routes for determining the number of available processors) +AC_CHECK_FUNCS([sched_getaffinity]) + # Check for whether math library is required AC_CHECK_LIB([m], [cos], [MATH_LIBS=-lm], diff --git a/src/common-ssh/key.c b/src/common-ssh/key.c index 4a88aead2..a7f861d89 100644 --- a/src/common-ssh/key.c +++ b/src/common-ssh/key.c @@ -132,18 +132,25 @@ guac_common_ssh_key* guac_common_ssh_key_alloc(char* data, int length, * different key algorithms) we need to perform a heuristic here to check * if a passphrase is needed. This could allow junk keys through that * would never be able to auth. libssh2 should display errors to help - * admins track down malformed keys and delete or replace them. - */ + * admins track down malformed keys and delete or replace them. */ if (is_passphrase_needed(data, length) && (passphrase == NULL || *passphrase == '\0')) return NULL; guac_common_ssh_key* key = guac_mem_alloc(sizeof(guac_common_ssh_key)); + /* NOTE: Older versions of libssh2 will at times ignore the declared key + * length and instead recalculate the length using strlen(). This has since + * been fixed, but as of this writing the fix has not yet been released. + * Below, we add our own null terminator to ensure that such calls to + * strlen() will work without issue. We can remove this workaround once + * copies of libssh2 that use strlen() on key data are not in common use. */ + /* Copy private key to structure */ key->private_key_length = length; - key->private_key = guac_mem_alloc(length); + key->private_key = guac_mem_alloc(guac_mem_ckd_add_or_die(length, 1)); /* Extra byte added here for null terminator (see above) */ memcpy(key->private_key, data, length); + key->private_key[length] = '\0'; /* Manually-added null terminator (see above) */ key->passphrase = guac_strdup(passphrase); return key; diff --git a/src/common/Makefile.am b/src/common/Makefile.am index 947104aeb..6f17b6538 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -35,7 +35,6 @@ noinst_HEADERS = \ common/clipboard.h \ common/cursor.h \ common/defaults.h \ - common/display.h \ common/dot_cursor.h \ common/ibar_cursor.h \ common/iconv.h \ @@ -51,7 +50,6 @@ libguac_common_la_SOURCES = \ blank_cursor.c \ clipboard.c \ cursor.c \ - display.c \ dot_cursor.c \ ibar_cursor.c \ iconv.c \ diff --git a/src/common/common/display.h b/src/common/common/display.h deleted file mode 100644 index 92999930b..000000000 --- a/src/common/common/display.h +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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. - */ - -#ifndef GUAC_COMMON_DISPLAY_H -#define GUAC_COMMON_DISPLAY_H - -#include "cursor.h" -#include "surface.h" - -#include -#include - -#include - -/** - * The minimum display size (height or width) in pixels. - */ -#define GUAC_COMMON_DISPLAY_MIN_SIZE 200 - -/** - * The maximum display size (height or width) in pixels. - */ -#define GUAC_COMMON_DISPLAY_MAX_SIZE 8192 - -/** - * The minimum amount of time that must elapse between display size updates, - * in milliseconds. - */ -#define GUAC_COMMON_DISPLAY_UPDATE_INTERVAL 500 - -/** - * A list element representing a pairing of a Guacamole layer with a - * corresponding guac_common_surface which wraps that layer. Adjacent layers - * within the same list are pointed to with traditional prev/next pointers. The - * order of layers in lists need not correspond in any way to the natural - * ordering of those layers' indexes nor their stacking order (Z-order) within - * the display. - */ -typedef struct guac_common_display_layer guac_common_display_layer; - -struct guac_common_display_layer { - - /** - * A Guacamole layer. - */ - guac_layer* layer; - - /** - * The surface which wraps the associated layer. - */ - guac_common_surface* surface; - - /** - * The layer immediately prior to this layer within the list containing - * this layer, or NULL if this is the first layer/buffer in the list. - */ - guac_common_display_layer* prev; - - /** - * The layer immediately following this layer within the list containing - * this layer, or NULL if this is the last layer/buffer in the list. - */ - guac_common_display_layer* next; - -}; - -/** - * Abstracts a remote Guacamole display, having an associated client, - * default surface, mouse cursor, and various allocated buffers and layers. - */ -typedef struct guac_common_display { - - /** - * The client associate with this display. - */ - guac_client* client; - - /** - * The default surface of the client display. - */ - guac_common_surface* default_surface; - - /** - * Client-wide cursor, synchronized across all users. - */ - guac_common_cursor* cursor; - - /** - * The first element within a linked list of all currently-allocated - * layers, or NULL if no layers are currently allocated. The default layer, - * layer #0, is stored within default_surface and will not have a - * corresponding element within this list. - */ - guac_common_display_layer* layers; - - /** - * The first element within a linked list of all currently-allocated - * buffers, or NULL if no buffers are currently allocated. - */ - guac_common_display_layer* buffers; - - /** - * Non-zero if all graphical updates for this display should use lossless - * compression, 0 otherwise. By default, newly-created displays will use - * lossy compression when heuristics determine it is appropriate. - */ - int lossless; - - /** - * Mutex which is locked internally when access to the display must be - * synchronized. All public functions of guac_common_display should be - * considered threadsafe. - */ - pthread_mutex_t _lock; - -} guac_common_display; - -/** - * Allocates a new display, abstracting the cursor and buffer/layer allocation - * operations of the given guac_client such that client state can be easily - * synchronized to joining users. - * - * @param client - * The guac_client to associate with this display. - * - * @param width - * The initial width of the display, in pixels. - * - * @param height - * The initial height of the display, in pixels. - * - * @return - * The newly-allocated display. - */ -guac_common_display* guac_common_display_alloc(guac_client* client, - int width, int height); - -/** - * Fits a given dimension within the allowed bounds for display sizing, - * adjusting the other dimension such that aspect ratio is maintained. - * - * @param a - * The dimension to fit within allowed bounds. - * - * @param b - * The other dimension to adjust if and only if necessary to preserve - * aspect ratio. - */ -void guac_common_display_fit(int* a, int* b); - -/** - * Frees the given display, and any associated resources, including any - * allocated buffers/layers. - * - * @param display - * The display to free. - */ -void guac_common_display_free(guac_common_display* display); - -/** - * Duplicates the state of the given display to the given socket. Any pending - * changes to buffers, layers, or the default layer are not flushed. - * - * @param display - * The display whose state should be sent along the given socket. - * - * @param client - * The client associated with the users receiving the display state. - * - * @param socket - * The socket over which the display state should be sent. - */ -void guac_common_display_dup( - guac_common_display* display, guac_client* client, - guac_socket* socket); - -/** - * Flushes pending changes to the given display. All pending operations will - * become visible to any connected users. - * - * @param display - * The display to flush. - */ -void guac_common_display_flush(guac_common_display* display); - -/** - * Allocates a new layer, returning a new wrapped layer and corresponding - * surface. The layer may be reused from a previous allocation, if that layer - * has since been freed. - * - * @param display - * The display to allocate a new layer from. - * - * @param width - * The width of the layer to allocate, in pixels. - * - * @param height - * The height of the layer to allocate, in pixels. - * - * @return - * A newly-allocated layer. - */ -guac_common_display_layer* guac_common_display_alloc_layer( - guac_common_display* display, int width, int height); - -/** - * Allocates a new buffer, returning a new wrapped buffer and corresponding - * surface. The buffer may be reused from a previous allocation, if that buffer - * has since been freed. - * - * @param display - * The display to allocate a new buffer from. - * - * @param width - * The width of the buffer to allocate, in pixels. - * - * @param height - * The height of the buffer to allocate, in pixels. - * - * @return - * A newly-allocated buffer. - */ -guac_common_display_layer* guac_common_display_alloc_buffer( - guac_common_display* display, int width, int height); - -/** - * Frees the given surface and associated layer, returning the layer to the - * given display for future use. - * - * @param display - * The display originally allocating the layer. - * - * @param display_layer - * The layer to free. - */ -void guac_common_display_free_layer(guac_common_display* display, - guac_common_display_layer* display_layer); - -/** - * Frees the given surface and associated buffer, returning the buffer to the - * given display for future use. - * - * @param display - * The display originally allocating the buffer. - * - * @param display_buffer - * The buffer to free. - */ -void guac_common_display_free_buffer(guac_common_display* display, - guac_common_display_layer* display_buffer); - -/** - * Sets the overall lossless compression policy of the given display to the - * given value, affecting all current and future layers/buffers maintained by - * the display. By default, newly-created displays will use lossy compression - * for graphical updates when heuristics determine that doing so is - * appropriate. Specifying a non-zero value here will force all graphical - * updates to always use lossless compression, whereas specifying zero will - * restore the default policy. - * - * Note that this can also be adjusted on a per-layer / per-buffer basis with - * guac_common_surface_set_lossless(). - * - * @param display - * The display to modify. - * - * @param lossless - * Non-zero if all graphical updates for this display should use lossless - * compression, 0 otherwise. - */ -void guac_common_display_set_lossless(guac_common_display* display, - int lossless); - - - -#endif - diff --git a/src/common/display.c b/src/common/display.c deleted file mode 100644 index a17b2ecca..000000000 --- a/src/common/display.c +++ /dev/null @@ -1,422 +0,0 @@ -/* - * 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 "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" - -#include -#include -#include - -#include -#include -#include - -/** - * Synchronizes all surfaces within the given linked list to the given socket. - * If the provided pointer to the linked list is NULL, this function has no - * effect. - * - * @param layers - * The head element of the linked list of layers to synchronize, which may - * be NULL if the list is currently empty. - * - * @param client - * The client associated with the users receiving the layers. - * - * @param socket - * The socket over which each layer should be sent. - */ -static void guac_common_display_dup_layers(guac_common_display_layer* layers, - guac_client* client, guac_socket* socket) { - - guac_common_display_layer* current = layers; - - /* Synchronize all surfaces in given list */ - while (current != NULL) { - guac_common_surface_dup(current->surface, client, socket); - current = current->next; - } - -} - -/** - * Frees all layers and associated surfaces within the given list, as well as - * their corresponding list elements. If the provided pointer to the linked - * list is NULL, this function has no effect. - * - * @param layers - * The head element of the linked list of layers to free, which may be NULL - * if the list is currently empty. - * - * @param client - * The client owning the layers wrapped by each of the layers in the list. - */ -static void guac_common_display_free_layers(guac_common_display_layer* layers, - guac_client* client) { - - guac_common_display_layer* current = layers; - - /* Free each surface in given list */ - while (current != NULL) { - - guac_common_display_layer* next = current->next; - guac_layer* layer = current->layer; - - /* Free surface */ - guac_common_surface_free(current->surface); - - /* Destroy layer within remotely-connected client */ - guac_protocol_send_dispose(client->socket, layer); - - /* Free layer or buffer depending on index */ - if (layer->index < 0) - guac_client_free_buffer(client, layer); - else if (layer->index > 0) - guac_client_free_layer(client, layer); - - /* Free current element and advance to next */ - guac_mem_free(current); - current = next; - - } - -} - -/** - * Allocates a display and a cursor which are used to represent the remote - * display and cursor. - * - * @param client - * The client owning the cursor. - * - * @param width - * The desired width of the display. - * - * @param height - * The desired height of the display. - * - * @return - * The newly-allocated display or NULL if display cannot be allocated. - */ -guac_common_display* guac_common_display_alloc(guac_client* client, - int width, int height) { - - /* Allocate display */ - guac_common_display* display = guac_mem_alloc(sizeof(guac_common_display)); - if (display == NULL) - return NULL; - - /* Allocate shared cursor */ - display->cursor = guac_common_cursor_alloc(client); - if (display->cursor == NULL) { - guac_mem_free(display); - return NULL; - } - - pthread_mutex_init(&display->_lock, NULL); - - /* Associate display with given client */ - display->client = client; - - display->default_surface = guac_common_surface_alloc(client, - client->socket, GUAC_DEFAULT_LAYER, width, height); - - /* No initial layers or buffers */ - display->layers = NULL; - display->buffers = NULL; - - return display; - -} - -void guac_common_display_free(guac_common_display* display) { - - /* Free shared cursor */ - guac_common_cursor_free(display->cursor); - - /* Free default surface */ - guac_common_surface_free(display->default_surface); - - /* Free all layers and buffers */ - guac_common_display_free_layers(display->buffers, display->client); - guac_common_display_free_layers(display->layers, display->client); - - pthread_mutex_destroy(&display->_lock); - guac_mem_free(display); - -} - -void guac_common_display_dup( - guac_common_display* display, guac_client* client, - guac_socket* socket) { - - pthread_mutex_lock(&display->_lock); - - /* Synchronize shared cursor */ - guac_common_cursor_dup(display->cursor, client, socket); - - /* Synchronize default surface */ - guac_common_surface_dup(display->default_surface, client, socket); - - /* Synchronize all layers and buffers */ - guac_common_display_dup_layers(display->layers, client, socket); - guac_common_display_dup_layers(display->buffers, client, socket); - - /* Sends a sync instruction to mark the boundary of the first frame */ - guac_protocol_send_sync(socket, client->last_sent_timestamp, 1); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_fit(int* a, int* b) { - - int a_value = *a; - int b_value = *b; - - /* Ensure first dimension is within allowed range */ - if (a_value < GUAC_COMMON_DISPLAY_MIN_SIZE) { - - /* Adjust other dimension to maintain aspect ratio */ - int adjusted_b = b_value * GUAC_COMMON_DISPLAY_MIN_SIZE / a_value; - if (adjusted_b > GUAC_COMMON_DISPLAY_MAX_SIZE) - adjusted_b = GUAC_COMMON_DISPLAY_MAX_SIZE; - - *a = GUAC_COMMON_DISPLAY_MIN_SIZE; - *b = adjusted_b; - - } - else if (a_value > GUAC_COMMON_DISPLAY_MAX_SIZE) { - - /* Adjust other dimension to maintain aspect ratio */ - int adjusted_b = b_value * GUAC_COMMON_DISPLAY_MAX_SIZE / a_value; - if (adjusted_b < GUAC_COMMON_DISPLAY_MIN_SIZE) - adjusted_b = GUAC_COMMON_DISPLAY_MIN_SIZE; - - *a = GUAC_COMMON_DISPLAY_MAX_SIZE; - *b = adjusted_b; - - } - -} - -void guac_common_display_set_lossless(guac_common_display* display, - int lossless) { - - pthread_mutex_lock(&display->_lock); - - /* Update lossless setting to be applied to all newly-allocated - * layers/buffers */ - display->lossless = lossless; - - /* Update losslessness of all allocated layers/buffers */ - guac_common_display_layer* current = display->layers; - while (current != NULL) { - guac_common_surface_set_lossless(current->surface, lossless); - current = current->next; - } - - /* Update losslessness of default display layer (not included within layers - * list) */ - guac_common_surface_set_lossless(display->default_surface, lossless); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_flush(guac_common_display* display) { - - pthread_mutex_lock(&display->_lock); - - guac_common_display_layer* current = display->layers; - - /* Flush all surfaces */ - while (current != NULL) { - guac_common_surface_flush(current->surface); - current = current->next; - } - - guac_common_surface_flush(display->default_surface); - - pthread_mutex_unlock(&display->_lock); - -} - -/** - * Allocates and inserts a new element into the given linked list of display - * layers, associating it with the given layer and surface. - * - * @param head - * A pointer to the head pointer of the list of layers. The head pointer - * will be updated by this function to point to the newly-allocated - * display layer. - * - * @param layer - * The Guacamole layer to associated with the new display layer. - * - * @param surface - * The surface associated with the given Guacamole layer and which should - * be associated with the new display layer. - * - * @return - * The newly-allocated display layer, which has been associated with the - * provided layer and surface. - */ -static guac_common_display_layer* guac_common_display_add_layer( - guac_common_display_layer** head, guac_layer* layer, - guac_common_surface* surface) { - - guac_common_display_layer* old_head = *head; - - guac_common_display_layer* display_layer = - guac_mem_alloc(sizeof(guac_common_display_layer)); - - /* Init layer/surface pair */ - display_layer->layer = layer; - display_layer->surface = surface; - - /* Insert list element as the new head */ - display_layer->prev = NULL; - display_layer->next = old_head; - *head = display_layer; - - /* Update old head to point to new element, if it existed */ - if (old_head != NULL) - old_head->prev = display_layer; - - return display_layer; - -} - -/** - * Removes the given display layer from the linked list whose head pointer is - * provided. - * - * @param head - * A pointer to the head pointer of the list of layers. The head pointer - * will be updated by this function if necessary, and will be set to NULL - * if the display layer being removed is the only layer in the list. - * - * @param display_layer - * The display layer to remove from the given list. - */ -static void guac_common_display_remove_layer(guac_common_display_layer** head, - guac_common_display_layer* display_layer) { - - /* Update previous element, if it exists */ - if (display_layer->prev != NULL) - display_layer->prev->next = display_layer->next; - - /* If there is no previous element, update the list head */ - else - *head = display_layer->next; - - /* Update next element, if it exists */ - if (display_layer->next != NULL) - display_layer->next->prev = display_layer->prev; - -} - -guac_common_display_layer* guac_common_display_alloc_layer( - guac_common_display* display, int width, int height) { - - pthread_mutex_lock(&display->_lock); - - /* Allocate Guacamole layer */ - guac_layer* layer = guac_client_alloc_layer(display->client); - - /* Allocate corresponding surface */ - guac_common_surface* surface = guac_common_surface_alloc(display->client, - display->client->socket, layer, width, height); - - /* Apply current display losslessness */ - guac_common_surface_set_lossless(surface, display->lossless); - - /* Add layer and surface to list */ - guac_common_display_layer* display_layer = - guac_common_display_add_layer(&display->layers, layer, surface); - - pthread_mutex_unlock(&display->_lock); - return display_layer; - -} - -guac_common_display_layer* guac_common_display_alloc_buffer( - guac_common_display* display, int width, int height) { - - pthread_mutex_lock(&display->_lock); - - /* Allocate Guacamole buffer */ - guac_layer* buffer = guac_client_alloc_buffer(display->client); - - /* Allocate corresponding surface */ - guac_common_surface* surface = guac_common_surface_alloc(display->client, - display->client->socket, buffer, width, height); - - /* Apply current display losslessness */ - guac_common_surface_set_lossless(surface, display->lossless); - - /* Add buffer and surface to list */ - guac_common_display_layer* display_layer = - guac_common_display_add_layer(&display->buffers, buffer, surface); - - pthread_mutex_unlock(&display->_lock); - return display_layer; - -} - -void guac_common_display_free_layer(guac_common_display* display, - guac_common_display_layer* display_layer) { - - pthread_mutex_lock(&display->_lock); - - /* Remove list element from list */ - guac_common_display_remove_layer(&display->layers, display_layer); - - /* Free associated layer and surface */ - guac_common_surface_free(display_layer->surface); - guac_client_free_layer(display->client, display_layer->layer); - - /* Free list element */ - guac_mem_free(display_layer); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_free_buffer(guac_common_display* display, - guac_common_display_layer* display_buffer) { - - pthread_mutex_lock(&display->_lock); - - /* Remove list element from list */ - guac_common_display_remove_layer(&display->buffers, display_buffer); - - /* Free associated layer and surface */ - guac_common_surface_free(display_buffer->surface); - guac_client_free_buffer(display->client, display_buffer->layer); - - /* Free list element */ - guac_mem_free(display_buffer); - - pthread_mutex_unlock(&display->_lock); - -} diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 2386f9c31..daf148198 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -39,16 +39,25 @@ libguacinc_HEADERS = \ guacamole/argv.h \ guacamole/argv-constants.h \ guacamole/argv-fntypes.h \ + guacamole/assert.h \ guacamole/audio.h \ guacamole/audio-fntypes.h \ guacamole/audio-types.h \ - guacamole/client-constants.h \ guacamole/client.h \ + guacamole/client-constants.h \ guacamole/client-fntypes.h \ guacamole/client-types.h \ + guacamole/display.h \ + guacamole/display-constants.h \ + guacamole/display-types.h \ guacamole/error.h \ guacamole/error-types.h \ + guacamole/fifo.h \ + guacamole/fifo-constants.h \ + guacamole/fifo-types.h \ guacamole/fips.h \ + guacamole/flag.h \ + guacamole/flag-types.h \ guacamole/hash.h \ guacamole/layer.h \ guacamole/layer-types.h \ @@ -66,9 +75,11 @@ libguacinc_HEADERS = \ guacamole/protocol-constants.h \ guacamole/protocol-types.h \ guacamole/recording.h \ + guacamole/rect.h \ + guacamole/rect-types.h \ guacamole/rwlock.h \ - guacamole/socket-constants.h \ guacamole/socket.h \ + guacamole/socket-constants.h \ guacamole/socket-fntypes.h \ guacamole/socket-types.h \ guacamole/stream.h \ @@ -98,46 +109,64 @@ libguacprivinc_HEADERS = \ # Private, non-installed headers # -noinst_HEADERS = \ - id.h \ - encode-jpeg.h \ - encode-png.h \ - palette.h \ - user-handlers.h \ - raw_encoder.h \ +noinst_HEADERS = \ + display-builtin-cursors.h \ + display-plan.h \ + display-priv.h \ + encode-jpeg.h \ + encode-png.h \ + id.h \ + palette.h \ + raw_encoder.h \ + user-handlers.h \ wait-fd.h -libguac_la_SOURCES = \ - argv.c \ - audio.c \ - client.c \ - encode-jpeg.c \ - encode-png.c \ - error.c \ - fips.c \ - hash.c \ - id.c \ - mem.c \ - rwlock.c \ - palette.c \ - parser.c \ - pool.c \ - protocol.c \ - raw_encoder.c \ - recording.c \ - socket.c \ - socket-broadcast.c \ - socket-fd.c \ - socket-nest.c \ - socket-tee.c \ - string.c \ - tcp.c \ - timestamp.c \ - unicode.c \ - user.c \ - user-handlers.c \ - user-handshake.c \ - wait-fd.c \ +libguac_la_SOURCES = \ + argv.c \ + audio.c \ + client.c \ + display.c \ + display-builtin-cursors.c \ + display-cursor.c \ + display-flush.c \ + display-layer.c \ + display-layer-list.c \ + display-plan.c \ + display-plan-combine.c \ + display-plan-rect.c \ + display-plan-search.c \ + display-render-thread.c \ + display-worker.c \ + encode-jpeg.c \ + encode-png.c \ + error.c \ + fifo.c \ + fips.c \ + flag.c \ + hash.c \ + id.c \ + mem.c \ + rwlock.c \ + palette.c \ + parser.c \ + pool.c \ + protocol.c \ + raw_encoder.c \ + recording.c \ + rect.c \ + socket.c \ + socket-broadcast.c \ + socket-fd.c \ + socket-nest.c \ + socket-tee.c \ + string.c \ + tcp.c \ + timestamp.c \ + unicode.c \ + user.c \ + user-handlers.c \ + user-handshake.c \ + wait-fd.c \ wol.c # Compile WebP support if available diff --git a/src/libguac/client.c b/src/libguac/client.c index 508f175ee..5926e9ca6 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -48,10 +48,10 @@ #include /** - * The number of nanoseconds between times that the pending users list will be + * The number of milliseconds between times that the pending users list will be * synchronized and emptied (250 milliseconds aka 1/4 second). */ -#define GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL 250000000 +#define GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL 250 /** * A value that indicates that the pending users timer has yet to be @@ -127,13 +127,11 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { guac_stream* allocd_stream; int stream_index; - /* Refuse to allocate beyond maximum */ - if (client->__stream_pool->active == GUAC_CLIENT_MAX_STREAMS) + /* Allocate stream, but refuse to allocate beyond maximum */ + stream_index = guac_pool_next_int_below(client->__stream_pool, GUAC_CLIENT_MAX_STREAMS); + if (stream_index < 0) return NULL; - /* Allocate stream */ - stream_index = guac_pool_next_int(client->__stream_pool); - /* Initialize stream with odd index (even indices are user-level) */ allocd_stream = &(client->__output_streams[stream_index]); allocd_stream->index = (stream_index * 2) + 1; @@ -148,40 +146,23 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { void guac_client_free_stream(guac_client* client, guac_stream* stream) { - /* Release index to pool */ - guac_pool_free_int(client->__stream_pool, (stream->index - 1) / 2); - /* Mark stream as closed */ + int freed_index = stream->index; stream->index = GUAC_CLIENT_CLOSED_STREAM_INDEX; + /* Release index to pool */ + guac_pool_free_int(client->__stream_pool, (freed_index - 1) / 2); + } /** * Promote all pending users to full users, calling the join pending handler * before, if any. * - * @param data + * @param client * The client for which all pending users should be promoted. */ -static void guac_client_promote_pending_users(union sigval data) { - - guac_client* client = (guac_client*) data.sival_ptr; - - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - - /* Check if the previous instance of this handler is still running */ - int already_running = ( - client->__pending_users_timer_state - == GUAC_CLIENT_PENDING_TIMER_TRIGGERED); - - /* Mark the handler as running if it isn't already */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_TRIGGERED; - - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - - /* Do not start the handler if the previous instance is still running */ - if (already_running) - return; +static void guac_client_promote_pending_users(guac_client* client) { /* Acquire the lock for reading and modifying the list of pending users */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); @@ -244,10 +225,29 @@ static void guac_client_promote_pending_users(union sigval data) { * to ensure that all users are always on exactly one of these lists) */ guac_rwlock_release_lock(&(client->__pending_users_lock)); - /* Mark the handler as complete so the next instance can run */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); +} + +/** + * Thread that periodically checks for users that have requested to join the + * current connection (pending users). The check is performed every + * GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL milliseconds. + * + * @param data + * A pointer to the guac_client associated with the connection. + * + * @return + * Always NULL. + */ +static void* guac_client_pending_users_thread(void* data) { + + guac_client* client = (guac_client*) data; + + while (client->state == GUAC_CLIENT_RUNNING) { + guac_client_promote_pending_users(client); + guac_timestamp_msleep(GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL); + } + + return NULL; } @@ -295,12 +295,6 @@ guac_client* guac_client_alloc() { guac_rwlock_init(&(client->__users_lock)); guac_rwlock_init(&(client->__pending_users_lock)); - /* The timer will be lazily created in the child process */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_UNREGISTERED; - - /* Set up the pending user promotion mutex */ - pthread_mutex_init(&(client->__pending_users_timer_mutex), NULL); - /* Set up broadcast sockets */ client->socket = guac_socket_broadcast(client); client->pending_socket = guac_socket_broadcast_pending(client); @@ -311,6 +305,9 @@ guac_client* guac_client_alloc() { void guac_client_free(guac_client* client) { + /* Ensure that anything waiting for the client can begin shutting down */ + guac_client_stop(client); + /* Acquire write locks before referencing user pointers */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); guac_rwlock_acquire_write_lock(&(client->__users_lock)); @@ -323,6 +320,11 @@ void guac_client_free(guac_client* client) { while (client->__users != NULL) guac_client_remove_user(client, client->__users); + /* Clean up the thread monitoring for new pending users, if it's been + * started */ + if (client->__pending_users_thread_started) + pthread_join(client->__pending_users_thread, NULL); + /* Release the locks */ guac_rwlock_release_lock(&(client->__users_lock)); guac_rwlock_release_lock(&(client->__pending_users_lock)); @@ -354,19 +356,6 @@ void guac_client_free(guac_client* client) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to close plugin: %s", dlerror()); } - /* Find out if the pending user promotion timer was ever started */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - int was_started = ( - client->__pending_users_timer_state - != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - - /* If the timer was registered, stop it before destroying the lock */ - if (was_started) - timer_delete(client->__pending_users_timer); - - pthread_mutex_destroy(&(client->__pending_users_timer_mutex)); - /* Destroy the reentrant read-write locks */ guac_rwlock_destroy(&(client->__users_lock)); guac_rwlock_destroy(&(client->__pending_users_lock)); @@ -444,12 +433,19 @@ void guac_client_abort(guac_client* client, guac_protocol_status status, * @param user * The user to add to the pending list. */ -static void guac_client_add_pending_user( - guac_client* client, guac_user* user) { +static void guac_client_add_pending_user(guac_client* client, + guac_user* user) { /* Acquire the lock for modifying the list of pending users */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); + /* Set up the pending user promotion mutex */ + if (!client->__pending_users_thread_started) { + pthread_create(&client->__pending_users_thread, NULL, + guac_client_pending_users_thread, (void*) client); + client->__pending_users_thread_started = 1; + } + user->__prev = NULL; user->__next = client->__pending_users; @@ -466,82 +462,8 @@ static void guac_client_add_pending_user( } -/** - * Periodically promote pending users to full users. Returns zero if the timer - * is already running, or successfully created, or a non-zero value if the - * timer could not be created and started. - * - * @param client - * The guac client for which the new timer should be started, if not - * already running. - * - * @return - * Zero if the timer was successfully created and started, or a negative - * value otherwise. - */ -static int guac_client_start_pending_users_timer(guac_client* client) { - - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - - /* Return success if the timer is already created and running */ - if (client->__pending_users_timer_state - != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 0; - } - - /* Configure the timer to synchronize and clear the pending users */ - struct sigevent signal_config = { - .sigev_notify = SIGEV_THREAD, - .sigev_notify_function = guac_client_promote_pending_users, - .sigev_value = { .sival_ptr = client }}; - - /* Create a timer to synchronize any pending users periodically */ - if (timer_create( - CLOCK_MONOTONIC, - &signal_config, - &(client->__pending_users_timer))) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 1; - } - - /* Configure the pending users timer to run on the defined interval */ - struct itimerspec time_config = { - .it_interval = { .tv_nsec = GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL }, - .it_value = { .tv_nsec = GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL } - }; - - /* Start the timer */ - if (timer_settime( - client->__pending_users_timer, 0, &time_config, NULL) < 0) { - timer_delete(client->__pending_users_timer); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 1; - } - - /* Mark the timer as registered but not yet running */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 0; - -} - int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** argv) { - /* Create and start the timer if it hasn't already been initialized */ - if (guac_client_start_pending_users_timer(client)) { - - /** - * - * If the timer could not be created, do not add the user - they cannot - * be synchronized without the timer. - */ - guac_client_log(client, GUAC_LOG_ERROR, - "Could not start pending user timer: %s.", strerror(errno)); - return 1; - } - int retval = 0; /* Call handler, if defined */ diff --git a/src/libguac/display-builtin-cursors.c b/src/libguac/display-builtin-cursors.c new file mode 100644 index 000000000..a5d12fa9c --- /dev/null +++ b/src/libguac/display-builtin-cursors.c @@ -0,0 +1,149 @@ +/* + * 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-builtin-cursors.h" + +/** + * Opaque black. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque black and is expected to be used + * only within this file to help make embedded cursor graphics more readable. + */ +#define X 0x00,0x00,0x00,0xFF + +/** + * Opaque gray. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque gray and is expected to be used only + * within this file to help make embedded cursor graphics more readable. + */ +#define U 0x80,0x80,0x80,0xFF + +/** + * Opaque white. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque white and is expected to be used + * only within this file to help make embedded cursor graphics more readable. + */ +#define O 0xFF,0xFF,0xFF,0xFF + +/** + * Full transparency. This macro evaluates to the 4 bytes of the single pixel + * of a 32-bit ARGB image that represent full transparency and is expected to + * be used only within this file to help make embedded cursor graphics more + * readable. + */ +#define _ 0x00,0x00,0x00,0x00 + +const guac_display_builtin_cursor guac_display_cursor_none = { + + .hotspot_x = 0, + .hotspot_y = 0, + + .buffer = (unsigned char[]) { + _ /* Single, transparent pixel */ + }, + + .width = 1, + .height = 1, + .stride = 4 + +}; + +const guac_display_builtin_cursor guac_display_cursor_dot = { + + .hotspot_x = 2, + .hotspot_y = 2, + + .buffer = (unsigned char[]) { + + _,O,O,O,_, + O,X,X,X,O, + O,X,X,X,O, + O,X,X,X,O, + _,O,O,O,_ + + }, + + .width = 5, + .height = 5, + .stride = 20 + +}; + +const guac_display_builtin_cursor guac_display_cursor_ibar = { + + .hotspot_x = 3, + .hotspot_y = 7, + + .buffer = (unsigned char[]) { + + X,X,X,X,X,X,X, + X,O,O,U,O,O,X, + X,X,X,O,X,X,X, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + X,X,X,O,X,X,X, + X,O,O,U,O,O,X, + X,X,X,X,X,X,X + + }, + + .width = 7, + .height = 16, + .stride = 28 + +}; + +const guac_display_builtin_cursor guac_display_cursor_pointer = { + + .hotspot_x = 0, + .hotspot_y = 0, + + .buffer = (unsigned char[]) { + + O,_,_,_,_,_,_,_,_,_,_, + O,O,_,_,_,_,_,_,_,_,_, + O,X,O,_,_,_,_,_,_,_,_, + O,X,X,O,_,_,_,_,_,_,_, + O,X,X,X,O,_,_,_,_,_,_, + O,X,X,X,X,O,_,_,_,_,_, + O,X,X,X,X,X,O,_,_,_,_, + O,X,X,X,X,X,X,O,_,_,_, + O,X,X,X,X,X,X,X,O,_,_, + O,X,X,X,X,X,X,X,X,O,_, + O,X,X,X,X,X,O,O,O,O,O, + O,X,X,O,X,X,O,_,_,_,_, + O,X,O,_,O,X,X,O,_,_,_, + O,O,_,_,O,X,X,O,_,_,_, + O,_,_,_,_,O,X,X,O,_,_, + _,_,_,_,_,O,O,O,O,_,_ + + }, + + .width = 11, + .height = 16, + .stride = 44 + +}; diff --git a/src/libguac/display-builtin-cursors.h b/src/libguac/display-builtin-cursors.h new file mode 100644 index 000000000..01ec38186 --- /dev/null +++ b/src/libguac/display-builtin-cursors.h @@ -0,0 +1,93 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_BUILTIN_CURSORS_H +#define GUAC_DISPLAY_BUILTIN_CURSORS_H + +#include + +/** + * Mouse cursor image that is built into libguac. Each actual instance of this + * structure will correspond to a value within the guac_display_cursor_type + * enum. + */ +typedef struct guac_display_builtin_cursor { + + /** + * The raw, 32-bit ARGB image for this mouse cursor. + */ + const unsigned char* const buffer; + + /** + * The width of this mouse cursor image, in pixels. + */ + const unsigned int width; + + /** + * The height of this mouse cursor image, in pixels. + */ + const unsigned int height; + + /** + * The size of each row of image data, in bytes. + */ + const size_t stride; + + /** + * The X coordinate of the relative position of the pointer hotspot within + * the cursor image. The hotspot is the location that the mouse pointer is + * actually reported, with the cursor image visibly positioned relative to + * that location. + */ + int hotspot_x; + + /** + * The Y coordinate of the relative position of the pointer hotspot within + * the cursor image. The hotspot is the location that the mouse pointer is + * actually reported, with the cursor image visibly positioned relative to + * that location. + */ + int hotspot_y; + +} guac_display_builtin_cursor; + +/** + * An empty (invisible/hidden) mouse cursor. + */ +extern const guac_display_builtin_cursor guac_display_cursor_none; + +/** + * A small dot. This is typically used in situations where cursor information + * for the remote desktop is not available, thus all cursor rendering must + * happen remotely, but it's still important that the user be able to see the + * current location of their local mouse pointer. + */ +extern const guac_display_builtin_cursor guac_display_cursor_dot; + +/** + * A vertical, I-shaped bar indicating text input or selection. + */ +extern const guac_display_builtin_cursor guac_display_cursor_ibar; + +/** + * A standard, general-purpose pointer. + */ +extern const guac_display_builtin_cursor guac_display_cursor_pointer; + +#endif diff --git a/src/libguac/display-cursor.c b/src/libguac/display-cursor.c new file mode 100644 index 000000000..6d119caa9 --- /dev/null +++ b/src/libguac/display-cursor.c @@ -0,0 +1,104 @@ +/* + * 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-builtin-cursors.h" +#include "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/display.h" +#include "guacamole/mem.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" + +#include + +guac_display_layer* guac_display_cursor(guac_display* display) { + return display->cursor_buffer; +} + +void guac_display_set_cursor_hotspot(guac_display* display, int x, int y) { + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + display->pending_frame.cursor_hotspot_x = x; + display->pending_frame.cursor_hotspot_y = y; + + guac_rwlock_release_lock(&display->pending_frame.lock); +} + +void guac_display_set_cursor(guac_display* display, + guac_display_cursor_type cursor_type) { + + /* Translate requested type into built-in cursor */ + const guac_display_builtin_cursor* cursor; + switch (cursor_type) { + + case GUAC_DISPLAY_CURSOR_NONE: + cursor = &guac_display_cursor_none; + break; + + case GUAC_DISPLAY_CURSOR_DOT: + cursor = &guac_display_cursor_dot; + break; + + case GUAC_DISPLAY_CURSOR_IBAR: + cursor = &guac_display_cursor_ibar; + break; + + case GUAC_DISPLAY_CURSOR_POINTER: + default: + cursor = &guac_display_cursor_pointer; + break; + + } + + /* Resize cursor to fit requested icon */ + guac_display_layer* cursor_layer = guac_display_cursor(display); + guac_display_layer_resize(cursor_layer, cursor->width, cursor->height); + + /* Copy over graphical content of cursor icon ... */ + + guac_display_layer_raw_context* context = guac_display_layer_open_raw(cursor_layer); + GUAC_ASSERT(!cursor_layer->pending_frame.buffer_is_external); + + const unsigned char* src_cursor_row = cursor->buffer; + unsigned char* dst_cursor_row = context->buffer; + size_t row_length = guac_mem_ckd_mul_or_die(cursor->width, 4); + + for (int y = 0; y < cursor->height; y++) { + memcpy(dst_cursor_row, src_cursor_row, row_length); + src_cursor_row += cursor->stride; + dst_cursor_row += context->stride; + } + + /* ... and cursor hotspot */ + guac_display_set_cursor_hotspot(display, cursor->hotspot_x, cursor->hotspot_y); + + /* Update to cursor icon is now complete - notify display */ + + context->dirty = (guac_rect) { + .left = 0, + .top = 0, + .right = cursor->width, + .bottom = cursor->height + }; + + guac_display_layer_close_raw(cursor_layer, context); + + guac_display_end_mouse_frame(display); + +} diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c new file mode 100644 index 000000000..44436168a --- /dev/null +++ b/src/libguac/display-flush.c @@ -0,0 +1,391 @@ +/* + * 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 + +/** + * 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); + + 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; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Skip processing any layers whose buffers have 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); + 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(¤t->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; + + } + + /* 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; + + /* Duplicate layers from pending frame to last frame */ + current->last_frame.prev = current->pending_frame.prev; + current->last_frame.next = current->pending_frame.next; + current = current->pending_frame.next; + + } + + 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); + + retval = 1; + + } + + 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_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); + + /* Not all frames are graphical. If we end up with a frame containing + * nothing but layer property changes, then we must still send a frame + * boundary even though there is no display plan to optimize. */ + if (plan == NULL && frame_nonempty) { + guac_display_plan_operation end_frame_op = { + .type = GUAC_DISPLAY_PLAN_END_FRAME + }; + guac_fifo_enqueue(&display->ops, &end_frame_op); + } + + guac_rwlock_release_lock(&display->last_frame.lock); + +finished_with_pending_frame_lock: + guac_rwlock_release_lock(&display->pending_frame.lock); + + if (plan != NULL) { + guac_display_plan_apply(plan); + guac_display_plan_free(plan); + } + +} diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c new file mode 100644 index 000000000..f80aa3a4e --- /dev/null +++ b/src/libguac/display-layer-list.c @@ -0,0 +1,396 @@ +/* + * 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-priv.h" +#include "guacamole/assert.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/layer.h" +#include "guacamole/mem.h" +#include "guacamole/rwlock.h" + +#include +#include +#include + +/** + * Performs a bulk copy of image data from a source buffer to a destination + * buffer. The two buffers need not match in size and stride. If the + * destination is smaller than the desired source, the source dimensions will + * be adjusted to fit the available space. + * + * @param dst + * A pointer to the first byte of image data in the destination buffer. + * + * @param dst_stride + * The number of bytes in each row of image data in the destination buffer. + * + * @param dst_width + * The width of the destination buffer relative to the provided first byte, + * in pixels. + * + * @param dst_height + * The height of the destination buffer relative to the provided first byte, + * in pixels. + * + * @param src + * A pointer to the first byte of image data in the source buffer. + * + * @param src_stride + * The number of bytes in each row of image data in the source buffer. + * + * @param src_width + * The width of the source buffer relative to the provided first byte, in + * pixels. If this value is larger than dst_width, it will be adjusted to + * fit the available space. + * + * @param src_height + * The height of the source buffer relative to the provided first byte, in + * pixels. If this value is larger than dst_height, it will be adjusted to + * fit the available space. + * + * @param pixel_size + * The size of each pixel of image data, in bytes. The size of each pixel + * in both the destination and source buffers must be identical. + */ +static void guac_imgcpy(void* dst, size_t dst_stride, int dst_width, int dst_height, + void* src, size_t src_stride, int src_width, int src_height, + size_t pixel_size) { + + int width = dst_width; + int height = dst_height; + + if (src_width < width) width = src_width; + if (src_height < height) height = src_height; + + GUAC_ASSERT(width >= 0); + GUAC_ASSERT(height >= 0); + + size_t length = guac_mem_ckd_mul_or_die(width, pixel_size); + + for (size_t i = 0; i < height; i++) { + memcpy(dst, src, length); + dst = ((char*) dst) + dst_stride; + src = ((char*) src) + src_stride; + } + +} + +/** + * Resizes the layer represented by the given pair of layer states to the given + * dimensions, allocating a larger underlying image buffer if necessary. If no + * image buffer has yet been allocated, an image buffer large enough to hold + * the given dimensions will be automatically allocated. + * + * This function DOES NOT resize the pending cells array, which is not stored + * on the guac_display_layer_state. When resizing a layer, the pending cells + * array must be separately resized with a call to + * PFW_guac_display_layer_pending_frame_cells_resize(). + * + * @param last_frame + * The guac_display_layer_state representing the state of the layer at the + * end of the last frame sent to connected clients. + * + * @param pending_frame + * The guac_display_layer_state representing the current pending state of + * the layer for the upcoming frame to be eventually sent to connected + * clients. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +static void XFW_guac_display_layer_buffer_resize(guac_display_layer_state* frame_state, + int width, int height) { + + /* We should never be trying to resize an externally-maintained buffer */ + GUAC_ASSERT(!frame_state->buffer_is_external); + + /* Round up to nearest multiple of resize factor */ + width = ((width + GUAC_DISPLAY_RESIZE_FACTOR - 1) / GUAC_DISPLAY_RESIZE_FACTOR) * GUAC_DISPLAY_RESIZE_FACTOR; + height = ((height + GUAC_DISPLAY_RESIZE_FACTOR - 1) / GUAC_DISPLAY_RESIZE_FACTOR) * GUAC_DISPLAY_RESIZE_FACTOR; + + /* Do nothing if size isn't actually changing */ + if (width == frame_state->buffer_width + && height == frame_state->buffer_height) + return; + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + unsigned char* buffer = guac_mem_zalloc(height, stride); + + /* Copy over data from old shared buffer, if that data exists and is + * relevant */ + + if (frame_state->buffer != NULL) { + + guac_imgcpy( + + /* Copy to newly-allocated frame buffer ... */ + buffer, stride, + width, height, + + /* ... from old frame buffer. */ + frame_state->buffer, frame_state->buffer_stride, + frame_state->buffer_width, frame_state->buffer_height, + + /* All pixels are 32-bit */ + GUAC_DISPLAY_LAYER_RAW_BPP); + + guac_mem_free(frame_state->buffer); + + } + + frame_state->buffer = buffer; + frame_state->buffer_width = width; + frame_state->buffer_height = height; + frame_state->buffer_stride = stride; + +} + +/** + * Fully initializes the last and pending frame states for a newly-allocated + * layer, including its underlying image buffers. + * + * @param last_frame + * The guac_display_layer_state representing the state of the layer at the + * end of the last frame sent to connected clients. + * + * @param pending_frame + * The guac_display_layer_state representing the current pending state of + * the layer for the upcoming frame to be eventually sent to connected + * clients. + */ +static void PFW_LFW_guac_display_layer_state_init(guac_display_layer_state* last_frame, + guac_display_layer_state* pending_frame) { + + last_frame->width = pending_frame->width = GUAC_DISPLAY_RESIZE_FACTOR; + last_frame->height = pending_frame->height = GUAC_DISPLAY_RESIZE_FACTOR; + last_frame->opacity = pending_frame->opacity = 0xFF; + last_frame->parent = pending_frame->parent = GUAC_DEFAULT_LAYER; + + XFW_guac_display_layer_buffer_resize(last_frame, + last_frame->width, last_frame->height); + + XFW_guac_display_layer_buffer_resize(pending_frame, + pending_frame->width, pending_frame->height); + +} + +/** + * Resizes the pending_frame_cells array of the given layer to the given + * dimensions. + * + * @param layer + * The layer whose pending_frame_cells array should be resized. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +static void PFW_guac_display_layer_pending_frame_cells_resize(guac_display_layer* layer, + int width, int height) { + + int new_pending_frame_cells_width = GUAC_DISPLAY_CELL_DIMENSION(width); + int new_pending_frame_cells_height = GUAC_DISPLAY_CELL_DIMENSION(height); + + /* Do nothing if size isn't actually changing */ + if (new_pending_frame_cells_width == layer->pending_frame_cells_width + && new_pending_frame_cells_height == layer->pending_frame_cells_height) + return; + + guac_display_layer_cell* new_pending_frame_cells = guac_mem_zalloc(sizeof(guac_display_layer_cell), + new_pending_frame_cells_width, new_pending_frame_cells_height); + + /* Copy existing cells over to new memory if present */ + if (layer->pending_frame_cells != NULL) { + + size_t new_stride = guac_mem_ckd_mul_or_die(new_pending_frame_cells_width, sizeof(guac_display_layer_cell)); + size_t old_stride = guac_mem_ckd_mul_or_die(layer->pending_frame_cells_width, sizeof(guac_display_layer_cell)); + + guac_imgcpy( + + /* Copy to newly-allocated pending frame cells ... */ + new_pending_frame_cells, new_stride, + new_pending_frame_cells_width, new_pending_frame_cells_height, + + /* ... from old pending frame cells. */ + layer->pending_frame_cells, old_stride, + layer->pending_frame_cells_width, layer->pending_frame_cells_height, + + /* All "pixels" are guac_display_layer_cell structures */ + sizeof(guac_display_layer_cell)); + + } + + guac_mem_free(layer->pending_frame_cells); + layer->pending_frame_cells = new_pending_frame_cells; + layer->pending_frame_cells_width = new_pending_frame_cells_width; + layer->pending_frame_cells_height = new_pending_frame_cells_height; + +} + +guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* layer, int opaque) { + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Init core layer members */ + guac_display_layer* display_layer = guac_mem_zalloc(sizeof(guac_display_layer)); + display_layer->display = display; + display_layer->layer = layer; + display_layer->opaque = opaque; + + /* Init tracking of pending and last frames (NOTE: We need not acquire the + * display-wide last_frame.lock here as this new layer will not actually be + * part of the last frame layer list until the pending frame is flushed) */ + PFW_LFW_guac_display_layer_state_init(&display_layer->last_frame, &display_layer->pending_frame); + display_layer->last_frame_buffer = guac_client_alloc_buffer(display->client); + PFW_guac_display_layer_pending_frame_cells_resize(display_layer, + display_layer->pending_frame.width, + display_layer->pending_frame.height); + + /* Insert list element as the new head */ + guac_display_layer* old_head = display->pending_frame.layers; + display_layer->pending_frame.prev = NULL; + display_layer->pending_frame.next = old_head; + display->pending_frame.layers = display_layer; + + /* Update old head to point to new element, if it existed */ + if (old_head != NULL) + old_head->pending_frame.prev = display_layer; + + guac_rwlock_release_lock(&display->pending_frame.lock); + + return display_layer; + +} + +void guac_display_remove_layer(guac_display_layer* display_layer) { + + guac_display* display = display_layer->display; + + /* + * Remove layer from pending frame + */ + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Update previous element, if it exists */ + if (display_layer->pending_frame.prev != NULL) + display_layer->pending_frame.prev->pending_frame.next = display_layer->pending_frame.next; + + /* If there is no previous element, then this element is the list head if + * the list has any elements at all. Update the list head accordingly. */ + else if (display->pending_frame.layers != NULL) { + GUAC_ASSERT(display->pending_frame.layers == display_layer); + display->pending_frame.layers = display_layer->pending_frame.next; + } + + /* Update next element, if it exists */ + if (display_layer->pending_frame.next != NULL) + display_layer->pending_frame.next->pending_frame.prev = display_layer->pending_frame.prev; + + guac_rwlock_release_lock(&display->pending_frame.lock); + + /* + * Remove layer from last frame + */ + + guac_rwlock_acquire_write_lock(&display->last_frame.lock); + + /* Update previous element, if it exists */ + if (display_layer->last_frame.prev != NULL) + display_layer->last_frame.prev->last_frame.next = display_layer->last_frame.next; + + /* If there is no previous element, then this element is the list head if + * the list has any elements at all. Update the list head accordingly. */ + else if (display->last_frame.layers != NULL) { + GUAC_ASSERT(display->last_frame.layers == display_layer); + display->last_frame.layers = display_layer->last_frame.next; + } + + /* Update next element, if it exists */ + if (display_layer->last_frame.next != NULL) + display_layer->last_frame.next->last_frame.prev = display_layer->last_frame.prev; + + guac_rwlock_release_lock(&display->last_frame.lock); + + /* + * Layer has now been removed from both pending and last frame lists and + * can be safely freed + */ + + guac_client* client = display->client; + guac_client_free_buffer(client, display_layer->last_frame_buffer); + + /* Release any Cairo resources */ + guac_display_layer_cairo_context* cairo_context = &(display_layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + + cairo_surface_destroy(cairo_context->surface); + cairo_context->surface = NULL; + + cairo_destroy(cairo_context->cairo); + cairo_context->cairo = NULL; + + } + + /* Free memory for underlying image surface and change tracking cells. Note + * that we do NOT free the associated memory for the pending frame if it + * was replaced with an external buffer. */ + + if (!display_layer->pending_frame.buffer_is_external) + guac_mem_free(display_layer->pending_frame.buffer); + + guac_mem_free(display_layer->last_frame.buffer); + guac_mem_free(display_layer->pending_frame_cells); + + guac_mem_free(display_layer); + +} + +void PFW_guac_display_layer_resize(guac_display_layer* layer, int width, int height) { + + /* Flush and destroy any cached Cairo context */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + + cairo_surface_flush(cairo_context->surface); + cairo_surface_destroy(cairo_context->surface); + cairo_destroy(cairo_context->cairo); + + cairo_context->surface = NULL; + cairo_context->cairo = NULL; + + } + + /* Skip resizing underlying buffer if it's the caller that's responsible + * for resizing the buffer */ + if (!layer->pending_frame.buffer_is_external) + XFW_guac_display_layer_buffer_resize(&layer->pending_frame, width, height); + + PFW_guac_display_layer_pending_frame_cells_resize(layer, width, height); + + layer->pending_frame.width = width; + layer->pending_frame.height = height; + +} diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c new file mode 100644 index 000000000..2c983a157 --- /dev/null +++ b/src/libguac/display-layer.c @@ -0,0 +1,324 @@ +/* + * 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-priv.h" +#include "guacamole/assert.h" +#include "guacamole/display.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" + +#include +#include +#include + +/** + * Notifies the display associated with the given layer that the given layer + * has been modified in some way for the current pending frame. If the layer is + * not the cursor layer, the pending_frame_dirty_excluding_mouse flag of the + * display is updated accordingly. + * + * @param layer + * The layer that was modified. + */ +static void PFW_guac_display_layer_touch(guac_display_layer* layer) { + + guac_display* display = layer->display; + + if (layer != display->cursor_buffer) + display->pending_frame_dirty_excluding_mouse = 1; + +} + +void guac_display_layer_get_bounds(guac_display_layer* layer, guac_rect* bounds) { + + guac_display* display = layer->display; + guac_rwlock_acquire_read_lock(&display->pending_frame.lock); + + *bounds = (guac_rect) { + .left = 0, + .top = 0, + .right = layer->pending_frame.width, + .bottom = layer->pending_frame.height + }; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_move(guac_display_layer* layer, int x, int y) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.x = x; + layer->pending_frame.y = y; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_stack(guac_display_layer* layer, int z) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.z = z; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_parent(guac_display_layer* layer, const guac_display_layer* parent) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.parent = parent->layer; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.opacity = opacity; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.lossless = lossless; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.touches = touches; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_resize(guac_display_layer* layer, int width, int height) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + PFW_guac_display_layer_resize(layer, width, height); + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_raw_context_set(guac_display_layer_raw_context* context, + const guac_rect* dst, uint32_t color) { + + size_t dst_stride = context->stride; + unsigned char* restrict dst_buffer = GUAC_DISPLAY_LAYER_RAW_BUFFER(context, *dst); + + for (int dy = dst->top; dy < dst->bottom; dy++) { + + uint32_t* dst_pixel = (uint32_t*) dst_buffer; + dst_buffer += dst_stride; + + for (int dx = dst->left; dx < dst->right; dx++) + *(dst_pixel++) = color; + + } + + guac_rect_extend(&(context->dirty), dst); + +} + +void guac_display_layer_raw_context_put(guac_display_layer_raw_context* context, + const guac_rect* dst, const void* restrict buffer, size_t stride) { + + size_t dst_stride = context->stride; + unsigned char* restrict dst_buffer = GUAC_DISPLAY_LAYER_RAW_BUFFER(context, *dst); + const unsigned char* restrict src_buffer = (const unsigned char*) buffer; + + size_t copy_length = guac_mem_ckd_mul_or_die(guac_rect_width(dst), + GUAC_DISPLAY_LAYER_RAW_BPP); + + for (int dy = dst->top; dy < dst->bottom; dy++) { + memcpy(dst_buffer, src_buffer, copy_length); + dst_buffer += dst_stride; + src_buffer += stride; + } + + guac_rect_extend(&(context->dirty), dst); + +} + +guac_display_layer_raw_context* guac_display_layer_open_raw(guac_display_layer* layer) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Flush any outstanding Cairo operations before directly accessing buffer */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) + cairo_surface_flush(cairo_context->surface); + + layer->pending_frame_raw_context = (guac_display_layer_raw_context) { + .buffer = layer->pending_frame.buffer, + .stride = layer->pending_frame.buffer_stride, + .dirty = { 0 }, + .hint_from = layer, + .bounds = { + .left = 0, + .top = 0, + .right = layer->pending_frame.buffer_width, + .bottom = layer->pending_frame.buffer_height + } + }; + + return &layer->pending_frame_raw_context; + +} + +void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_raw_context* context) { + + guac_display* display = layer->display; + + /* Replace buffer if requested with an external buffer. This intentionally + * falls through to the following buffer_is_external check to update the + * buffer details. */ + if (context->buffer != layer->pending_frame.buffer + && !layer->pending_frame.buffer_is_external) { + guac_mem_free(layer->pending_frame.buffer); + layer->pending_frame.buffer_is_external = 1; + } + + /* The details covering the structure of the buffer and the dimensions of + * the layer must be copied from the context if the buffer is external + * (there is no other way to resize a layer with an external buffer) */ + if (layer->pending_frame.buffer_is_external) { + + int width = guac_rect_width(&context->bounds); + if (width > GUAC_DISPLAY_MAX_WIDTH) + width = GUAC_DISPLAY_MAX_WIDTH; + + int height = guac_rect_height(&context->bounds); + if (height > GUAC_DISPLAY_MAX_HEIGHT) + height = GUAC_DISPLAY_MAX_HEIGHT; + + /* Release any Cairo surface that was created around the external + * buffer, in case the details of the buffer have now changed */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + cairo_surface_destroy(cairo_context->surface); + cairo_context->surface = NULL; + } + + layer->pending_frame.buffer = context->buffer; + layer->pending_frame.buffer_width = width; + layer->pending_frame.buffer_height = height; + layer->pending_frame.buffer_stride = context->stride; + + layer->pending_frame.width = layer->pending_frame.buffer_width; + layer->pending_frame.height = layer->pending_frame.buffer_height; + + } + + guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + PFW_guac_display_layer_touch(layer); + + /* Apply any hinting regarding scroll/copy optimization */ + if (context->hint_from != NULL) + context->hint_from->pending_frame.search_for_copies = 1; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_layer* layer) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* It is intentionally allowed that the pending frame buffer can be + * replaced with NULL to ensure that references to external buffers can be + * removed prior to guac_display being freed. If the buffer has been + * manually replaced with NULL, further use of that buffer via Cairo + * contexts is not safe nor allowed. */ + GUAC_ASSERT(layer->pending_frame.buffer != NULL); + + guac_display_layer_cairo_context* context = &(layer->pending_frame_cairo_context); + + context->dirty = (guac_rect) { 0 }; + context->hint_from = layer; + context->bounds = (guac_rect) { + .left = 0, + .top = 0, + .right = layer->pending_frame.buffer_width, + .bottom = layer->pending_frame.buffer_height + }; + + if (context->surface == NULL) { + + context->surface = cairo_image_surface_create_for_data( + layer->pending_frame.buffer, + layer->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + layer->pending_frame.buffer_width, + layer->pending_frame.buffer_height, + layer->pending_frame.buffer_stride); + + context->cairo = cairo_create(context->surface); + + } + + return context; + +} + +void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_layer_cairo_context* context) { + + guac_display* display = layer->display; + + guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + PFW_guac_display_layer_touch(layer); + + /* Apply any hinting regarding scroll/copy optimization */ + if (context->hint_from != NULL) + context->hint_from->pending_frame.search_for_copies = 1; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} diff --git a/src/libguac/display-plan-combine.c b/src/libguac/display-plan-combine.c new file mode 100644 index 000000000..5e663c206 --- /dev/null +++ b/src/libguac/display-plan-combine.c @@ -0,0 +1,334 @@ +/* + * 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/display.h" +#include "guacamole/rect.h" + +/** + * Returns whether the given rectangle crosses the boundaries of any two + * adjacent cells in a grid, where each cell in the grid is + * 2^GUAC_DISPLAY_MAX_COMBINED_SIZE pixels on each side. + * + * This function exists because combination of adjacent image updates is + * intentionally limited to a certain size in order to favor parallelism. + * Greedily combining in the horizontal direction works, but in practice tends + * to produce a vertical series of strips that are offset from each other to + * the point that they cannot be further combined. Anchoring combined image + * updates to a grid helps prevent ths. + * + * @param rect + * The rectangle to test. + * + * @return + * Non-zero if the rectangle crosses the boundary of any adjacent pair of + * cells in a grid, where each cell is 2^GUAC_DISPLAY_MAX_COMBINED_SIZE + * pixels on each side, zero otherwise. + */ +static int guac_display_plan_rect_crosses_boundary(const guac_rect* rect) { + + /* A particular rectangle crosses a grid boundary if and only if expanding + * that rectangle to fit the grid would mean increasing the size of that + * rectangle beyond a single grid cell */ + + guac_rect rect_copy = *rect; + guac_rect_align(&rect_copy, GUAC_DISPLAY_MAX_COMBINED_SIZE); + + const int max_size_pixels = 1 << GUAC_DISPLAY_MAX_COMBINED_SIZE; + return guac_rect_width(&rect_copy) > max_size_pixels + || guac_rect_height(&rect_copy) > max_size_pixels; + +} + +/** + * Returns whether the two rectangles are adjacent and share exactly one common + * edge. + * + * @param op_a + * One of the rectangles to compare. + * + * @param op_b + * The rectangle to compare op_a with. + * + * @return + * Non-zero if the rectangles are adjacent and share exactly one common + * edge, zero otherwise. + */ +static int guac_display_plan_has_common_edge(const guac_display_plan_operation* op_a, + const guac_display_plan_operation* op_b) { + + /* Two operations share a common edge if they are perfectly aligned + * vertically and have the same left/right or right/left edge */ + if (op_a->dest.top == op_b->dest.top + && op_a->dest.bottom == op_b->dest.bottom) { + + return op_a->dest.right == op_b->dest.left + || op_a->dest.left == op_b->dest.right; + + } + + /* Two operations share a common edge if they are perfectly aligned + * horizontally and have the same top/bottom or bottom/top edge */ + else if (op_a->dest.left == op_b->dest.left + && op_a->dest.right == op_b->dest.right) { + + return op_a->dest.top == op_b->dest.bottom + || op_a->dest.bottom == op_b->dest.top; + + } + + /* There are no other cases where two operations share a common edge */ + return 0; + +} + +/** + * Returns whether the given pair of operations should be combined into a + * single operation. + * + * @param op_a + * The first operation to check. + * + * @param op_b + * The second operation to check. + * + * @return + * Non-zero if the operations would be better represented as a single, + * combined operation, zero otherwise. + */ +static int guac_display_plan_should_combine(const guac_display_plan_operation* op_a, + const guac_display_plan_operation* op_b) { + + /* Operations can only be combined within the same layer */ + if (op_a->layer != op_b->layer) + return 0; + + /* Simulate combination */ + guac_rect combined = op_a->dest; + guac_rect_extend(&combined, &op_b->dest); + + /* Operations of the same type can be trivially unified under specific + * circumstances */ + if (op_a->type == op_b->type) { + switch (op_a->type) { + + /* Copy operations can be combined if they are perfectly adjacent + * (exactly share an edge) and copy from the same source layer in + * the same direction */ + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + if (op_a->src.layer_rect.layer == op_b->src.layer_rect.layer + && guac_display_plan_has_common_edge(op_a, op_b)) { + + int delta_xa = op_a->dest.left - op_a->src.layer_rect.rect.left; + int delta_ya = op_a->dest.top - op_a->src.layer_rect.rect.top; + int delta_xb = op_b->dest.left - op_b->src.layer_rect.rect.left; + int delta_yb = op_b->dest.top - op_b->src.layer_rect.rect.top; + + return delta_xa == delta_xb + && delta_ya == delta_yb + && !guac_display_plan_rect_crosses_boundary(&combined); + + } + break; + + /* Rectangle-drawing operations can be combined if they are + * perfectly adjacent (exactly share an edge) and draw the same + * color */ + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + return op_a->src.color == op_b->src.color + && guac_display_plan_has_common_edge(op_a, op_b) + && !guac_display_plan_rect_crosses_boundary(&combined); + + /* Image-drawing operations can be combined if doing so wouldn't + * exceed the size limits for images (we enforce size limits here + * to promote parallelism) */ + case GUAC_DISPLAY_PLAN_OPERATION_IMG: + return !guac_display_plan_rect_crosses_boundary(&combined); + + /* Other combinations require more complex logic... (see below) */ + default: + break; + + } + } + + /* Combine if result is still small */ + int combined_width = guac_rect_width(&combined); + int combined_height = guac_rect_height(&combined); + if (combined_width <= GUAC_DISPLAY_NEGLIGIBLE_WIDTH && combined_height <= GUAC_DISPLAY_NEGLIGIBLE_HEIGHT) + return 1; + + /* Estimate costs of the existing update, new update, and both combined */ + int cost_ab = GUAC_DISPLAY_BASE_COST + combined_width * combined_height; + int cost_a = GUAC_DISPLAY_BASE_COST + op_a->dirty_size; + int cost_b = GUAC_DISPLAY_BASE_COST + op_b->dirty_size; + + /* Reduce cost if no image data */ + if (op_a->type != GUAC_DISPLAY_PLAN_OPERATION_IMG) cost_a /= GUAC_DISPLAY_DATA_FACTOR; + if (op_b->type != GUAC_DISPLAY_PLAN_OPERATION_IMG) cost_b /= GUAC_DISPLAY_DATA_FACTOR; + + /* Combine if cost estimate shows benefit or the increase in cost is + * negligible */ + if ((cost_ab <= cost_b + cost_a) + || (cost_ab - cost_a <= cost_a / GUAC_DISPLAY_NEGLIGIBLE_INCREASE) + || (cost_ab - cost_b <= cost_b / GUAC_DISPLAY_NEGLIGIBLE_INCREASE)) + return 1; + + /* Otherwise, do not combine */ + return 0; + +} + +/** + * Combines the given pair of operations into a single operation if doing so is + * advantageous (results in an operation of lesser or negligibly-worse cost). + * + * @param op_a + * The first of the pair of operations to be combined. If they operations + * are combined, the combined operation will be stored here. + * + * @param op_b + * The second of the pair of operations to be combined, which may + * potentially be identical to the first. If the operations are combined, + * this operation will be updated to be a GUAC_DISPLAY_PLAN_OPERATION_NOP + * operation. + * + * @return + * Non-zero if the operations were combined, zero otherwise. + */ +static int guac_display_plan_combine_if_improved(guac_display_plan_operation* op_a, + guac_display_plan_operation* op_b) { + + if (op_a == op_b) + return 0; + + /* Combine any adjacent operations that match the combination criteria + * (combining produces a net lower cost) */ + if (guac_display_plan_should_combine(op_a, op_b)) { + + guac_rect_extend(&op_a->dest, &op_b->dest); + + /* Operations of different types can only be combined as images */ + if (op_a->type != op_b->type) + op_a->type = GUAC_DISPLAY_PLAN_OPERATION_IMG; + + /* When combining two copy operations, additionally combine their + * source rects (NOT just the destination rects) */ + else if (op_a->type == GUAC_DISPLAY_PLAN_OPERATION_COPY) + guac_rect_extend(&op_a->src.layer_rect.rect, &op_b->src.layer_rect.rect); + + op_a->dirty_size += op_b->dirty_size; + + if (op_b->last_frame > op_a->last_frame) + op_a->last_frame = op_b->last_frame; + + op_b->type = GUAC_DISPLAY_PLAN_OPERATION_NOP; + + return 1; + + } + + return 0; + +} + +void PFW_guac_display_plan_combine_horizontally(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Process only layers that have been modified */ + if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + + /* Loop through all cells in left-to-right, top-to-bottom order, + * combining any operations that are combinable and horizontally + * adjacent. */ + + guac_display_layer_cell* cell = current->pending_frame_cells; + for (int y = 0; y < current->pending_frame_cells_height; y++) { + + guac_display_layer_cell* previous = cell++; + for (int x = 1; x < current->pending_frame_cells_width; x++) { + + /* Combine adjacent updates if doing so is advantageous */ + if (previous->related_op != NULL && cell->related_op != NULL + && guac_display_plan_combine_if_improved(previous->related_op, cell->related_op)) { + cell->related_op = previous->related_op; + } + + previous++; + cell++; + + } + } + + } + + current = current->pending_frame.next; + + } + +} + +void PFW_guac_display_plan_combine_vertically(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Process only layers that have been modified */ + if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + + /* Loop through all cells in top-to-bottom, left-to-right order, + * combining any operations that are combinable and horizontally + * adjacent. */ + + guac_display_layer_cell* cell_col = current->pending_frame_cells; + for (int x = 0; x < current->pending_frame_cells_width; x++) { + + guac_display_layer_cell* previous = cell_col; + guac_display_layer_cell* cell = cell_col + current->pending_frame_cells_width; + + for (int y = 1; y < current->pending_frame_cells_height; y++) { + + /* Combine adjacent updates if doing so is advantageous */ + if (previous->related_op != NULL && cell->related_op != NULL + && guac_display_plan_has_common_edge(previous->related_op, cell->related_op) + && guac_display_plan_combine_if_improved(previous->related_op, cell->related_op)) { + cell->related_op = previous->related_op; + } + + previous += current->pending_frame_cells_width; + cell += current->pending_frame_cells_width; + + } + + cell_col++; + + } + + } + + current = current->pending_frame.next; + + } + +} diff --git a/src/libguac/display-plan-rect.c b/src/libguac/display-plan-rect.c new file mode 100644 index 000000000..c2a4196a2 --- /dev/null +++ b/src/libguac/display-plan-rect.c @@ -0,0 +1,240 @@ +/* + * 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/display.h" +#include "guacamole/mem.h" +#include "guacamole/rect.h" + +#include +#include + +/** + * Rounds the given value down to the nearest power of two. + * + * @param value + * The value to round. + * + * @return + * The power of two that is closest to the given value without exceeding + * that value. + */ +static size_t guac_display_plan_round_pot(size_t value) { + + if (value <= 2) + return value; + + size_t rounded = 1; + while (value >>= 1) + rounded <<= 1; + + return rounded; + +} + +/** + * Returns whether the given buffer consists entirely of the same 32-bit + * quantity (ie: a single ARGB pixel), repeated throughout the buffer. + * + * This function attempts to perform a fast comparison leveraging memcmp() to + * reduce the search space, rather than simply looping through each pixel one + * at a time. Basic benchmarks show this approach to be roughly twice as fast + * as a simple loop for arbitrary buffer lengths and four times as fast for + * buffer lengths that are powers of two. + * + * @param buffer + * The buffer to check. + * + * @param length + * The number of bytes in the buffer. + * + * @param color + * A pointer to a uint32_t to receive the value of the 32-bit quantity that + * is repeated, if applicable. + * + * @return + * Non-zero if the same 32-bit quantity is repeated throughout the buffer, + * zero otherwise. If the same value is indeed repeated throughout the + * buffer, that value is stored in the variable pointed to by the "color" + * pointer. If the value is not repeated, the variable pointed to by the + * "color" pointer is left untouched. + */ +static int guac_display_plan_is_single_color(const unsigned char* restrict buffer, + size_t length, uint32_t* restrict color) { + + /* It is vacuously true that all the 32-bit quantities in an empty buffer + * are the same */ + if (length == 0) { + *color = 0x00000000; + return 1; + } + + /* A single 32-bit value is the same as itself */ + if (length == 4) { + *color = ((const uint32_t*) buffer)[0]; + return 1; + } + + /* Simply directly compare if there are only two values */ + if (length == 8) { + uint32_t a = ((const uint32_t*) buffer)[0]; + uint32_t b = ((const uint32_t*) buffer)[1]; + if (a == b) { + *color = a; + return 1; + } + } + + /* For all other lengths, avoid comparing if finding a match is impossible. + * A buffer can consist entirely of the same 32-bit (4-byte) quantity + * repeated throughout the buffer only if that buffer's length is a + * multiple of 4. */ + if ((length % 4) != 0) + return 0; + + /* A buffer consists entirely of the same 32-bit quantity repeated + * throughout if (1) the two halves of the buffer are the same and (2) one + * of those halves is known to consist entirely of the same 32-bit quantity + * repeated throughout. */ + + size_t pot_length = guac_display_plan_round_pot(guac_mem_ckd_sub_or_die(length, 1)); + size_t remaining_length = guac_mem_ckd_sub_or_die(length, pot_length); + + /* Easiest recursive case: the buffer is already a power of two and can be + * split into two very easy-to-compare halves */ + if (pot_length == remaining_length) { + return !memcmp(buffer, buffer + pot_length, pot_length) + && guac_display_plan_is_single_color(buffer, pot_length, color); + } + + /* For buffers that can't be split into two power-of-two halves, decide + * based on one easy power-of-two case and one not-so-easy case of whatever + * remains */ + uint32_t color_a = 0, color_b = 0; + if (guac_display_plan_is_single_color(buffer, pot_length, &color_a) + && guac_display_plan_is_single_color(buffer + pot_length, remaining_length, &color_b) + && color_a == color_b) { + + *color = color_a; + return 1; + + } + + return 0; + +} + +/** + * Returns whether the given rectangle within given buffer consists entirely of + * the same 32-bit quantity (ie: a single ARGB pixel), repeated throughout the + * rectangular region. + * + * This function attempts to perform a fast comparison leveraging memcmp() to + * reduce the search space, rather than simply looping through each pixel one + * at a time. Basic benchmarks show this approach to be roughly twice as fast + * as a simple loop for arbitrary buffer lengths and four times as fast for + * buffer lengths that are powers of two. + * + * @param buffer + * The buffer to check. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param rect + * The rectangle representing the region to be checked within the buffer. + * + * @param color + * A pointer to a uint32_t to receive the value of the 32-bit quantity that + * is repeated, if applicable. + * + * @return + * Non-zero if the same 32-bit quantity is repeated throughout the + * rectangular region, zero otherwise. If the same value is indeed repeated + * throughout the rectangle, that value is stored in the variable pointed + * to by the "color" pointer. If the value is not repeated, the variable + * pointed to by the "color" pointer is left untouched. + */ +static int guac_display_plan_is_rect_single_color(const unsigned char* restrict buffer, + size_t stride, const guac_rect* restrict rect, uint32_t* restrict color) { + + size_t row_length = guac_mem_ckd_mul_or_die(guac_rect_width(rect), GUAC_DISPLAY_LAYER_RAW_BPP); + buffer = GUAC_RECT_CONST_BUFFER(*rect, buffer, stride, GUAC_DISPLAY_LAYER_RAW_BPP); + + /* Verify that the first row consists of a single color */ + uint32_t first_color = 0x00000000; + if (!guac_display_plan_is_single_color(buffer, row_length, &first_color)) + return 0; + + /* The whole rectangle consists of a single color if each row is identical + * and it's already known that one of those rows consists of the a single + * color */ + const unsigned char* previous = buffer; + for (int y = rect->top + 1; y < rect->bottom; y++) { + + const unsigned char* current = previous + stride; + if (memcmp(previous, current, row_length)) + return 0; + + previous = current; + + } + + *color = first_color; + return 1; + +} + +void PFR_guac_display_plan_rewrite_as_rects(guac_display_plan* plan) { + + uint32_t color = 0x00000000; + + guac_display_plan_operation* op = plan->ops; + for (int i = 0; i < plan->length; i++) { + + if (op->type == GUAC_DISPLAY_PLAN_OPERATION_IMG) { + + guac_display_layer* layer = op->layer; + size_t stride = layer->pending_frame.buffer_stride; + const unsigned char* buffer = layer->pending_frame.buffer; + + /* NOTE: Processing of operations referring to layers whose buffers + * have been replaced with NULL is intentionally allowed to ensure + * references to external buffers can be safely removed if + * necessary, even before guac_display is freed */ + + if (buffer != NULL && guac_display_plan_is_rect_single_color(buffer, stride, &op->dest, &color)) { + + /* Ignore alpha channel for opaque layers */ + if (layer->opaque) + color |= 0xFF000000; + + op->type = GUAC_DISPLAY_PLAN_OPERATION_RECT; + op->src.color = color; + + } + + } + + op++; + + } + +} diff --git a/src/libguac/display-plan-search.c b/src/libguac/display-plan-search.c new file mode 100644 index 000000000..0d4c241be --- /dev/null +++ b/src/libguac/display-plan-search.c @@ -0,0 +1,436 @@ +/* + * 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/display.h" +#include "guacamole/rect.h" + +#include +#include + +/** + * Stores the given operation within the ops_by_hash table of the given display + * plan based on the given hash value. The hash function applied for storing + * the operation is GUAC_DISPLAY_PLAN_OPERATION_HASH(). If another operation is + * already stored at the same location within ops_by_hash, that operation will + * be replaced. + * + * @param plan + * The plan to store the operation within. + * + * @param hash + * The hash value to use to calculate the storage location. This value will + * be further hashed with GUAC_DISPLAY_PLAN_OPERATION_HASH(). + * + * @param op + * The operation to store. + */ +static void guac_display_plan_store_indexed_op(guac_display_plan* plan, uint64_t hash, + guac_display_plan_operation* op) { + + size_t index = GUAC_DISPLAY_PLAN_OPERATION_HASH(hash); + guac_display_plan_indexed_operation* entry = &(plan->ops_by_hash[index]); + + if (entry->op == NULL) { + entry->hash = hash; + entry->op = op; + } + +} + +/** + * Removes and returns a pointer to the matching operation stored within the + * ops_by_hash table of the given display plan, if any. If no such operation is + * stored, NULL is returned. + * + * @param plan + * The plan to retrieve the operation from. + * + * @param hash + * The hash value to use to calculate the storage location. This value will + * be further hashed with GUAC_DISPLAY_PLAN_OPERATION_HASH(). + * + * @return + * The operation that was stored under the given hash, if any, or NULL if + * no such operation was found. + */ +static guac_display_plan_operation* guac_display_plan_remove_indexed_op(guac_display_plan* plan, uint64_t hash) { + + size_t index = GUAC_DISPLAY_PLAN_OPERATION_HASH(hash); + guac_display_plan_indexed_operation* entry = &(plan->ops_by_hash[index]); + + /* NOTE: We verify the hash value here because the lookup performed is + * actually a hash of a hash. There's an additional chance of collisions + * between hash values at this second level of hashing. */ + + guac_display_plan_operation* op = entry->op; + if (op != NULL && entry->hash == hash) { + entry->op = NULL; + return op; + } + + return NULL; + +} + +/** + * Callback invoked by guac_hash_foreach_image_rect() for each 64x64 rectangle + * of image data. + * + * @param plan + * The display plan related to the call to guac_hash_foreach_image_rect(). + * + * @param x + * The X coordinate of the upper-left corner of the current 64x64 rectangle + * within the search region. + * + * @param y + * The Y coordinate of the upper-left corner of the current 64x64 rectangle + * within the search region. + * + * @param hash + * The hash value that applies to the current 64x64 rectangle. + * + * @param closure + * The closure value that was originally provided to the call to + * guac_hash_foreach_image_rect(). + */ +typedef void guac_hash_callback(guac_display_plan* plan, int x, int y, uint64_t hash, void* closure); + +/** + * Iterates through each 64x64 subrectangle within the given rectangular region + * of the underlying buffer of the given layer state, invoking the given + * callback for each such subrectangle. Each 64x64 subrectangle within the + * rectangular region is evaluated by sliding a 64x64 window over each pixel of + * the region such that every 64x64 subrectangle in the region is eventually + * covered. + * + * @param plan + * The display plan related to the search/indexing operation being + * performed. + * + * @param layer_state + * The layer state containing the image buffer to hash. + * + * @param rect + * The rectangular region within the image buffer that should be hashed. + * + * @param callback + * The callback to invoke for each 64x64 subrectangle of the given region. + * + * @param closure + * The arbitrary value to pass the given callback each time it is invoked + * through this function call. + */ +static int guac_hash_foreach_image_rect(guac_display_plan* plan, + const guac_display_layer_state* layer_state, const guac_rect* rect, + guac_hash_callback* callback, void* closure) { + + size_t stride = layer_state->buffer_stride; + const unsigned char* data = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(*layer_state, *rect); + + int x, y; + uint64_t cell_hash[GUAC_DISPLAY_MAX_WIDTH] = { 0 }; + + /* NOTE: Because the hash value of the sliding 64x64 window is available + * only upon reaching the bottom-right corner of that window, we offset the + * coordinates here by the relative location of the bottom-right corner + * (GUAC_DISPLAY_CELL_SIZE - 1) so that we have easy access to the + * coordinates of the upper-left corner of the sliding window, as required + * by the callback being invoked. + * + * This also allows us to easily determine when the hash is valid and it's + * safe to invoke the callback. Once the coordinates are within the given + * rect, we have evaluated a full 64x64 rectangle and have a valid hash. */ + + int start_x = rect->left - GUAC_DISPLAY_CELL_SIZE + 1; + int end_x = rect->right - GUAC_DISPLAY_CELL_SIZE + 1; + int start_y = rect->top - GUAC_DISPLAY_CELL_SIZE + 1; + int end_y = rect->bottom - GUAC_DISPLAY_CELL_SIZE + 1; + + for (y = start_y; y < end_y; y++) { + + uint64_t* current_cell_hash = cell_hash; + + /* Get current row */ + uint32_t* row = (uint32_t*) data; + data += stride; + + /* Calculate row segment hashes for entire row */ + uint64_t row_hash = 0; + for (x = start_x; x < end_x; x++) { + + /* Get current pixel */ + uint32_t pixel = *(row++); + + /* Update hash value for current row segment */ + row_hash = ((row_hash * 31) << 1) + pixel; + + /* Incorporate row hash value into overall cell hash */ + uint64_t cell_hash = ((*current_cell_hash * 31) << 1) + row_hash; + *(current_cell_hash++) = cell_hash; + + /* Invoke callback for every hash generated, breaking out early if + * requested */ + if (y >= rect->top && x >= rect->left) + callback(plan, x, y, cell_hash, closure); + + } + + } /* end for each row */ + + return 0; + +} + +/** + * Initializes the given rectangle with the bounds of the pending frame cell + * containing the given coordinate. + * + * @param rect + * The rectangle to initialize. + * + * @param x + * The X coordinate of the point that the rectangle must contain. + * + * @param y + * The Y coordinate of the point that the rectangle must contain. + */ +static void guac_display_cell_init_rect(guac_rect* rect, int x, int y) { + x = (x / GUAC_DISPLAY_CELL_SIZE) * GUAC_DISPLAY_CELL_SIZE; + y = (y / GUAC_DISPLAY_CELL_SIZE) * GUAC_DISPLAY_CELL_SIZE; + guac_rect_init(rect, x, y, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE); +} + +/** + * Callback for guac_hash_foreach_image_rect() which stores the given operation + * in the ops_by_hash table of the given display plan. + * + * @param plan + * The display plan to store the given operation in. + * + * @param x + * The X coordinate of the upper-left corner of the 64x64 rectangle + * modified by the given operation. + * + * @param y + * The Y coordinate of the upper-left corner of the 64x64 rectangle + * modified by the given operation. + * + * @param hash + * The hash value that applies to the 64x64 rectangle at the given + * coordinates. + * + * @param closure + * A pointer to the guac_display_plan_operation that should be stored + * within the ops_by_hash table of the given display plan. + */ +static void guac_display_plan_index_op_for_cell(guac_display_plan* plan, int x, int y, uint64_t hash, void* closure) { + guac_display_plan_store_indexed_op(plan, hash, (guac_display_plan_operation*) closure); +} + +void PFR_guac_display_plan_index_dirty_cells(guac_display_plan* plan) { + + memset(plan->ops_by_hash, 0, sizeof(plan->ops_by_hash)); + + guac_display_plan_operation* op = plan->ops; + for (int i = 0; i < plan->length; i++) { + + if (op->type == GUAC_DISPLAY_PLAN_OPERATION_IMG) { + + guac_display_layer* layer = op->layer; + + guac_rect layer_bounds; + guac_display_layer_get_bounds(layer, &layer_bounds); + + guac_rect cell; + guac_display_cell_init_rect(&cell, op->dest.left, op->dest.top); + + guac_rect_constrain(&cell, &layer_bounds); + if (guac_rect_width(&cell) == GUAC_DISPLAY_CELL_SIZE + && guac_rect_height(&cell) == GUAC_DISPLAY_CELL_SIZE) { + guac_hash_foreach_image_rect(plan, &layer->pending_frame, + &cell, guac_display_plan_index_op_for_cell, op); + } + + } + + op++; + + } + +} + +/** + * Compares two rectangular regions of two arbitrary buffers, returning whether + * those regions contain identical data. + * + * @param data_a + * A pointer to the first byte of image data within the first region being + * compared. + * + * @param width_a + * The width of the first region, in pixels. + * + * @param height_a + * The height of the first region, in pixels. + * + * @param stride_a + * The number of bytes in each row of image data in the first region. + * + * @param data_b + * A pointer to the first byte of image data within the second region being + * compared. + * + * @param width_b + * The width of the second region, in pixels. + * + * @param height_b + * The height of the second region, in pixels. + * + * @param stride_b + * The number of bytes in each row of image data in the first region. + * + * @return + * Non-zero if the regions contain at least one differing pixel, zero + * otherwise. + */ +static int guac_image_cmp(const unsigned char* restrict data_a, int width_a, int height_a, + int stride_a, const unsigned char* restrict data_b, int width_b, int height_b, + int stride_b) { + + int y; + + /* If core dimensions differ, just compare those. Done. */ + if (width_a != width_b) return width_a - width_b; + if (height_a != height_b) return height_a - height_b; + + size_t length = guac_mem_ckd_mul_or_die(width_a, GUAC_DISPLAY_LAYER_RAW_BPP); + + for (y = 0; y < height_a; y++) { + + /* Compare row. If different, use that result. */ + int cmp_result = memcmp(data_a, data_b, length); + if (cmp_result != 0) + return cmp_result; + + /* Next row */ + data_a += stride_a; + data_b += stride_b; + + } + + /* Otherwise, same. */ + return 0; + +} + +/** + * Callback for guac_hash_foreach_image_rect() which searches the ops_by_hash + * table of the given display plan for occurrences of the given hash, replacing + * the matching operation with a copy operation if a match is found. + * + * NOTE: While this function will search for and optimize operations that copy + * existing data, it can only do so for distinct image data. Multiple + * operations that copy the same exact data (like a region tiled with multiple + * copies of some pattern) can only be stored in the table once, and therefore + * will only match once. + * + * @param plan + * The display plan to update with any copies found. + * + * @param x + * The X coordinate of the upper-left corner of the 64x64 region currently + * being checked. + * + * @param y + * The Y coordinate of the upper-left corner of the 64x64 region currently + * being checked. + * + * @param hash + * The hash value that applies to the 64x64 rectangle at the given + * coordinates. + * + * @param closure + * A pointer to the guac_display_layer that is being searched. + */ +static void PFR_LFR_guac_display_plan_find_copies(guac_display_plan* plan, + int x, int y, uint64_t hash, void* closure) { + + guac_display_layer* copy_from_layer = (guac_display_layer*) closure; + + /* Transform the matching operation into a copy of the current region if + * any operations match, banning the underlying hash from further checks if + * a collision occurs */ + guac_display_plan_operation* op = guac_display_plan_remove_indexed_op(plan, hash); + if (op != NULL) { + + guac_display_layer* copy_to_layer = op->layer; + + guac_rect src_rect; + guac_rect_init(&src_rect, x, y, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE); + + guac_rect dst_rect; + guac_display_cell_init_rect(&dst_rect, op->dest.left, op->dest.top); + + const unsigned char* copy_from = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(copy_from_layer->last_frame, src_rect); + const unsigned char* copy_to = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(copy_to_layer->pending_frame, dst_rect); + + /* Only transform into a copy if the image data is truly identical (not a collision) */ + if (!guac_image_cmp(copy_from, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, copy_from_layer->last_frame.buffer_stride, + copy_to, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, copy_to_layer->pending_frame.buffer_stride)) { + op->type = GUAC_DISPLAY_PLAN_OPERATION_COPY; + op->src.layer_rect.layer = copy_from_layer->last_frame_buffer; + op->src.layer_rect.rect = src_rect; + op->dest = dst_rect; + } + + } + +} + +void PFR_LFR_guac_display_plan_rewrite_as_copies(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->last_frame.layers; + while (current != NULL) { + + /* Search only the layers that are specifically noted as possible + * sources for copies */ + if (current->pending_frame.search_for_copies) { + + guac_rect search_region; + guac_rect_init(&search_region, 0, 0, current->last_frame.width, current->last_frame.height); + + /* Avoid excessive computation by restricting the search region to only + * the area that was changed in the upcoming frame (in the case of + * scrolling, absolutely all data relevant to the scroll will have been + * modified) */ + guac_rect_constrain(&search_region, ¤t->pending_frame.dirty); + + guac_hash_foreach_image_rect(plan, ¤t->last_frame, &search_region, + PFR_LFR_guac_display_plan_find_copies, current); + } + + current = current->last_frame.next; + + } + +} diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c new file mode 100644 index 000000000..212d93461 --- /dev/null +++ b/src/libguac/display-plan.c @@ -0,0 +1,441 @@ +/* + * 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/mem.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" + +#include +#include + +/** + * Updates the dirty rect in the given cell to note that a horizontal line of + * image data at the given location and having the given width has changed + * since the last frame. A provided counter of the overall number of changed + * cells is updated accordingly. + * + * @param layer + * The layer that changed. + * + * @param cell + * The cell containing the line of image data that changed. + * + * @param count + * A pointer to a counter that contains the current number of cells that + * have been marked as having changed since the last frame. + * + * @param x + * The X coordinate of the leftmost pixel of the horizontal line. + * + * @param y + * The Y coordinate of the leftmost pixel of the horizontal line. + * + * @param width + * The width of the line, in pixels. + */ +static void guac_display_plan_mark_dirty(guac_display_layer* layer, + guac_display_layer_cell* cell, size_t* count, int x, int y, + int width) { + + if (!cell->dirty_size) { + guac_rect_init(&cell->dirty, x, y, width, 1); + cell->dirty_size = width; + (*count)++; + } + + else { + guac_rect dirty; + guac_rect_init(&dirty, x, y, width, 1); + guac_rect_extend(&cell->dirty, &dirty); + cell->dirty_size += width; + } + +} + +/** + * Variant of memcmp() which specifically compares series of 32-bit quantities + * and determines the overall location and length of the differences in the two + * provided buffers. The length and location determined are the length and + * location of the smallest contiguous series of 32-bit quantities that differ + * between the buffers. + * + * @param buffer_a + * The first buffer to compare. + * + * @param buffer_b + * The buffer to compare with buffer_a. + * + * @param count + * The number of 32-bit quantities in each buffer. + * + * @param pos + * A pointer to a size_t that should receive the offset of the difference, + * if the two buffers turn out to contain different data. The value of the + * size_t will only be modified if at least one difference is found. + * + * @return + * The number of 32-bit quantities after and including the offset returned + * via pos that are different between buffer_a and buffer_b, or zero if + * there are no such differences. + */ +static size_t guac_display_memcmp(const uint32_t* restrict buffer_a, + const uint32_t* restrict buffer_b, size_t count, size_t* pos) { + + /* Locate first difference between the buffers, if any */ + size_t first = 0; + while (first < count) { + + if (*(buffer_a++) != *(buffer_b++)) + break; + + first++; + + } + + /* If we reached the end without finding any differences, no need to search + * further - the buffers are identical */ + if (first >= count) + return 0; + + /* Search through all remaining values in the buffers for the last + * difference (which may be identical to the first) */ + size_t last = first; + size_t offset = first + 1; + while (offset < count) { + + if (*(buffer_a++) != *(buffer_b++)) + last = offset; + + offset++; + + } + + /* Final difference found - provide caller with the starting offset and + * length (in 32-bit quantities) of differences */ + *pos = first; + return last - first + 1; + +} + +guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { + + guac_display_layer* current; + guac_timestamp frame_end = guac_timestamp_current(); + size_t op_count = 0; + + /* Loop through each layer, searching for modified regions */ + current = display->pending_frame.layers; + while (current != NULL) { + + /* Skip processing any layers whose buffers have 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); + continue; + } + + /* Check only within layer dirty region, skipping the layer if + * unmodified. This pass should reset and refine that region, but + * otherwise rely on proper reporting of modified regions by callers of + * the open/close layer functions. */ + guac_rect dirty = current->pending_frame.dirty; + if (guac_rect_is_empty(&dirty)) { + current = current->pending_frame.next; + continue; + } + + /* Flush any outstanding Cairo operations before directly accessing buffer */ + guac_display_layer_cairo_context* cairo_context = &(current->pending_frame_cairo_context); + if (cairo_context->surface != NULL) + cairo_surface_flush(cairo_context->surface); + + /* Re-align the dirty rect with nearest multiple of 64 to ensure each + * step of the dirty rect refinement loop starts at the topmost + * boundary of a cell */ + guac_rect_align(&dirty, GUAC_DISPLAY_CELL_SIZE_EXPONENT); + + guac_rect pending_frame_bounds = { + .left = 0, + .top = 0, + .right = current->pending_frame.width, + .bottom = current->pending_frame.height + }; + + /* Limit size of dirty rect by bounds of backing surface for pending + * frame ONLY (bounds checks against the last frame are performed + * within the loop such that everything outside the bounds of the last + * frame is considered dirty) */ + guac_rect_constrain(&dirty, &pending_frame_bounds); + + const unsigned char* flushed_row = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(current->last_frame, dirty); + unsigned char* buffer_row = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->pending_frame, dirty); + + guac_display_layer_cell* cell_row = current->pending_frame_cells + + guac_mem_ckd_mul_or_die(dirty.top / GUAC_DISPLAY_CELL_SIZE, current->pending_frame_cells_width) + + dirty.left / GUAC_DISPLAY_CELL_SIZE; + + /* Loop through the rough modified region, refining the dirty rects of + * each cell to more accurately contain only what has actually changed + * since last frame */ + current->pending_frame.dirty = (guac_rect) { 0 }; + for (int corner_y = dirty.top; corner_y < dirty.bottom; corner_y += GUAC_DISPLAY_CELL_SIZE) { + + int height = GUAC_DISPLAY_CELL_SIZE; + if (corner_y + height > dirty.bottom) + height = dirty.bottom - corner_y; + + /* Iteration through the pending_frame_cells array and the image + * buffer is a bit complex here, as the pending_frame_cells array + * contains cells that represent 64x64 regions, while the image + * buffers contain absolutely all pixels. The outer loop goes + * through just the pending cells, while the following loop goes + * through the Y coordinates that make up that cell. */ + + for (int y_off = 0; y_off < height; y_off++) { + + /* At this point, we need to loop through the horizontal + * dimension, comparing the 64-pixel rows of image data in the + * current line (corner_y + y_off) that are in each applicable + * cell. We jump forward by one cell for each comparison. */ + + int y = corner_y + y_off; + + guac_display_layer_cell* current_cell = cell_row; + uint32_t* current_flushed = (uint32_t*) flushed_row; + uint32_t* current_buffer = (uint32_t*) buffer_row; + for (int corner_x = dirty.left; corner_x < dirty.right; corner_x += GUAC_DISPLAY_CELL_SIZE) { + + int width = GUAC_DISPLAY_CELL_SIZE; + if (corner_x + width > dirty.right) + width = dirty.right - corner_x; + + /* This SHOULD be impossible, as corner_x would need to + * somehow be outside the bounds of the dirty rect, which + * would have failed the loop condition earlier) */ + GUAC_ASSERT(width >= 0); + + /* Any line that is completely outside the bounds of the + * previous frame is dirty (nothing to compare against) */ + if (y >= current->last_frame.height || corner_x >= current->last_frame.width) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x, y, width); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + /* All other regions must be processed further to determine + * what portion is dirty */ + else { + + /* Only the pixels that are within the bounds of BOTH + * the last_frame and pending_frame are directly + * comparable. Others are inherently dirty by virtue of + * being outside the bounds of last_frame */ + int comparable_width = width; + if (corner_x + comparable_width > current->last_frame.width) + comparable_width = current->last_frame.width - corner_x; + + /* It is impossible for this value to be negative + * because of the last_frame bounds checks that occur + * in the if block prior to this else block */ + GUAC_ASSERT(comparable_width >= 0); + + /* Any region outside the right edge of the previous frame is dirty */ + if (width > comparable_width) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x + comparable_width, y, width - comparable_width); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + /* Mark the relevant region of the cell as dirty if the + * current 64-pixel line has changed in any way */ + size_t length, pos; + if ((length = guac_display_memcmp(current_buffer, current_flushed, comparable_width, &pos)) != 0) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x + pos, y, length); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + } + + current_flushed += GUAC_DISPLAY_CELL_SIZE; + current_buffer += GUAC_DISPLAY_CELL_SIZE; + current_cell++; + + } + + flushed_row += current->last_frame.buffer_stride; + buffer_row += current->pending_frame.buffer_stride; + + } + + cell_row += current->pending_frame_cells_width; + + } + + current = current->pending_frame.next; + + } + + /* If no layer has been modified, there's no need to create a plan */ + if (!op_count) + return NULL; + + guac_display_plan* plan = guac_mem_alloc(sizeof(guac_display_plan)); + plan->display = display; + plan->frame_end = frame_end; + plan->length = guac_mem_ckd_add_or_die(op_count, 1); + plan->ops = guac_mem_alloc(plan->length, sizeof(guac_display_plan_operation)); + + /* Convert the dirty rectangles stored in each layer's cells to individual + * image operations for later optimization */ + size_t added_ops = 0; + guac_display_plan_operation* current_op = plan->ops; + current = display->pending_frame.layers; + while (current != NULL) { + + guac_display_layer_cell* cell = current->pending_frame_cells; + for (int y = 0; y < current->pending_frame_cells_height; y++) { + for (int x = 0; x < current->pending_frame_cells_width; x++) { + + if (cell->dirty_size) { + + /* The overall number of ops that we try to add via these + * nested loops should always exactly align with the + * anticipated count produced earlier and therefore not + * overrun the ops array at any point unless there is a bug + * in the way the original operation count was calculated */ + GUAC_ASSERT(added_ops < op_count); + + current_op->layer = current; + current_op->type = GUAC_DISPLAY_PLAN_OPERATION_IMG; + current_op->dest = cell->dirty; + current_op->dirty_size = cell->dirty_size; + current_op->last_frame = cell->last_frame; + current_op->current_frame = frame_end; + + cell->related_op = current_op; + cell->dirty_size = 0; + cell->last_frame = frame_end; + + current_op++; + added_ops++; + + } + else + cell->related_op = NULL; + + cell++; + + } + } + + current = current->pending_frame.next; + + } + + /* At this point, the number of operations added should exactly match the + * predicted quantity */ + GUAC_ASSERT(added_ops == op_count); + + /* Worker threads must be aware of end-of-frame to know when to send sync, + * etc. Noticing that the operation queue is empty is insufficient, as the + * queue may become empty while a frame is in progress if the worker + * threads happen to be processing things quickly. */ + current_op->type = GUAC_DISPLAY_PLAN_END_FRAME; + + return plan; + +} + +void guac_display_plan_free(guac_display_plan* plan) { + guac_mem_free(plan->ops); + guac_mem_free(plan); +} + +void guac_display_plan_apply(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_client* client = display->client; + guac_display_plan_operation* op = plan->ops; + + /* Do not allow worker threads to move forward with image encoding until + * AFTER the non-image instructions have finished being written */ + guac_fifo_lock(&display->ops); + + /* Immediately send instructions for all updates that do not involve + * significant processing (do not involve encoding anything). This allows + * us to use the worker threads solely for encoding, reducing contention + * between the threads. */ + for (int i = 0; i < plan->length; i++) { + + guac_display_layer* display_layer = op->layer; + switch (op->type) { + + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + guac_protocol_send_copy(client->socket, op->src.layer_rect.layer, + op->src.layer_rect.rect.left, op->src.layer_rect.rect.top, + guac_rect_width(&op->src.layer_rect.rect), guac_rect_height(&op->src.layer_rect.rect), + GUAC_COMP_OVER, display_layer->layer, op->dest.left, op->dest.top); + break; + + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + + guac_protocol_send_rect(client->socket, display_layer->layer, + op->dest.left, op->dest.top, guac_rect_width(&op->dest), guac_rect_height(&op->dest)); + + int alpha = (op->src.color & 0xFF000000) >> 24; + int red = (op->src.color & 0x00FF0000) >> 16; + int green = (op->src.color & 0x0000FF00) >> 8; + int blue = (op->src.color & 0x000000FF); + + /* Clear before drawing if layer is not opaque (transparency + * will not be copied correctly otherwise) */ + if (!display_layer->opaque) { + guac_protocol_send_cfill(client->socket, GUAC_COMP_ROUT, display_layer->layer, 0x00, 0x00, 0x00, 0xFF); + guac_protocol_send_cfill(client->socket, GUAC_COMP_OVER, display_layer->layer, red, green, blue, alpha); + } + else + guac_protocol_send_cfill(client->socket, GUAC_COMP_OVER, display_layer->layer, red, green, blue, 0xFF); + + break; + + /* Simply ignore and drop NOP */ + case GUAC_DISPLAY_PLAN_OPERATION_NOP: + break; + + /* All other operations should be handled by the workers */ + default: + guac_fifo_enqueue(&display->ops, op); + break; + + } + + op++; + + } + + guac_fifo_unlock(&display->ops); + +} diff --git a/src/libguac/display-plan.h b/src/libguac/display-plan.h new file mode 100644 index 000000000..d1a520b7f --- /dev/null +++ b/src/libguac/display-plan.h @@ -0,0 +1,399 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_PLAN_H +#define GUAC_DISPLAY_PLAN_H + +#include "guacamole/display.h" +#include "guacamole/rect.h" +#include "guacamole/timestamp.h" + +#include +#include + +/** + * The width of an update which should be considered negible and thus + * trivial overhead compared to the cost of two updates. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_WIDTH 64 + +/** + * The height of an update which should be considered negible and thus + * trivial overhead compared to the cost of two updates. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_HEIGHT 64 + +/** + * The proportional increase in cost contributed by transfer and processing of + * image data, compared to processing an equivalent amount of client-side + * data. + */ +#define GUAC_DISPLAY_DATA_FACTOR 128 + +/** + * The maximum width or height to allow when combining any pair of rendering + * operations into a single operation, in pixels, as the exponent of a power of + * two. This value is intended to be large enough to avoid unnecessarily + * increasing the number of drawing operations, yet also small enough to allow + * larger updates to be easily parallelized via the worker threads. + * + * The current value of 9 means that each encoded image will be no larger than + * 512x512 pixels. + */ +#define GUAC_DISPLAY_MAX_COMBINED_SIZE 9 + +/** + * The base cost of every update. Each update should be considered to have + * this starting cost, plus any additional cost estimated from its + * content. + */ +#define GUAC_DISPLAY_BASE_COST 4096 + +/** + * An increase in cost is negligible if it is less than + * 1/GUAC_DISPLAY_NEGLIGIBLE_INCREASE of the old cost. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_INCREASE 4 + +/** + * The framerate which, if exceeded, indicates that JPEG is preferred. + */ +#define GUAC_DISPLAY_JPEG_FRAMERATE 3 + +/** + * Minimum JPEG bitmap size (area). If the bitmap is smaller than this threshold, + * it should be compressed as a PNG image to avoid the JPEG compression tax. + */ +#define GUAC_DISPLAY_JPEG_MIN_BITMAP_SIZE 4096 + +/** + * The JPEG compression min block size, as the exponent of a power of two. This + * defines the optimal rectangle block size factor for JPEG compression. + * Usually 8x8 would suffice, but we use 16x16 here to reduce the occurrence of + * ringing artifacts further. + */ +#define GUAC_SURFACE_JPEG_BLOCK_SIZE 4 + +/** + * The WebP compression min block size, as the exponent of a power of two. This + * defines the optimal rectangle block size factor for WebP compression. WebP + * does utilize variable block size, but ensuring a block size factor reduces + * any noise on the image edges. + */ +#define GUAC_SURFACE_WEBP_BLOCK_SIZE 3 + +/** + * The number of hash buckets within each guac_display_plan. + */ +#define GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE 0x10000 + +/** + * Hash function which hashes a larger, 64-bit hash into a 16-bit hash that + * will fit within GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE. Note that the random + * distribution of this hash relies entirely on the random distribution of the + * value being hashed. + */ +#define GUAC_DISPLAY_PLAN_OPERATION_HASH(hash) (\ + ( hash & 0xFFFF) \ + ^ ((hash >> 16) & 0xFFFF) \ + ^ ((hash >> 32) & 0xFFFF) \ + ^ ((hash >> 48) & 0xFFFF) \ + ) + +/** + * The type of a graphical operation that may be part of a guac_display_plan. + */ +typedef enum guac_display_plan_operation_type { + + /** + * Do nothing (no-op). + */ + GUAC_DISPLAY_PLAN_OPERATION_NOP = 0, + + /** + * Copy image data from the associated source rect to the destination rect. + * The source and destination layers are not necessarily the same. + */ + GUAC_DISPLAY_PLAN_OPERATION_COPY, + + /** + * Fill a rectangular region of the destination layer with the source + * color. + */ + GUAC_DISPLAY_PLAN_OPERATION_RECT, + + /** + * Draw arbitrary image data to the destination rect. + */ + GUAC_DISPLAY_PLAN_OPERATION_IMG, + + /** + * Finish the frame, sending the frame boundary to all connected users. + */ + GUAC_DISPLAY_PLAN_END_FRAME + +} guac_display_plan_operation_type; + +/** + * A reference to a rectangular region of image data within a layer of the + * remote Guacamole display. + */ +typedef struct guac_display_plan_layer_rect { + + /** + * The rectangular region that should serve as source data for an + * operation. + */ + guac_rect rect; + + /** + * The layer that the source data is coming from. + */ + const guac_layer* layer; + +} guac_display_plan_layer_rect; + +/** + * Any one of several operations that may be contained in a guac_display_plan. + */ +typedef struct guac_display_plan_operation { + + /** + * The destination layer (recipient of graphical output/changes). + */ + guac_display_layer* layer; + + /** + * The operation being performed on the destination layer. + */ + guac_display_plan_operation_type type; + + /** + * The location within the destination layer that will receive these + * changes. + */ + guac_rect dest; + + /** + * The approximate number of pixels that have actually changed as a result + * of this operation. This value will not necessarily be the same as the + * area of the destination rect if some pixels remain unchanged. + */ + size_t dirty_size; + + /** + * The timestamp of the last frame that made any change within the + * destination rect of the destination layer. + */ + guac_timestamp last_frame; + + /** + * The timestamp of the change being made. This will be the timestamp of + * the frame at the time the frame was ended, not the timestamp of the + * server at the time this operation was added to the plan. + */ + guac_timestamp current_frame; + + union { + + /** + * The color that should be used to fill the destination rect. This + * value applies only to GUAC_DISPLAY_PLAN_OPERATION_RECT operations. + */ + uint32_t color; + + /** + * The rectangle that should be copied to the destination rect. This + * value applies only to GUAC_DISPLAY_PLAN_OPERATION_COPY operations. + */ + guac_display_plan_layer_rect layer_rect; + + } src; + +} guac_display_plan_operation; + +/** + * A guac_display_plan_operation that has been hashed and stored within a + * guac_display_plan. + */ +typedef struct guac_display_plan_indexed_operation { + + /** + * The operation. + */ + guac_display_plan_operation* op; + + /** + * The hash value associated with the operation. This hash value is derived + * from the actual image contents of the region that was changed, using the + * new contents of that region. The intent of this hash is to allow + * operations to be quickly located based on the output they will produce, + * such that image draw operations can be automatically replaced with + * simple copies if they reuse data from elsewhere in a layer. + */ + uint64_t hash; + +} guac_display_plan_indexed_operation; + +/** + * The set of operations required to transform the display state from what each + * user currently sees (the previous frame) to the current state of the + * guac_display (the current frame). The operations within a plan are quickly + * generated based on simple image comparisons, and are then refined by an + * optimizer based on estimated costs. + */ +typedef struct guac_display_plan { + + /** + * The display that this plan was created for. + */ + guac_display* display; + + /** + * The time that the frame ended. + */ + guac_timestamp frame_end; + + /** + * Array of all operations that should be applied, in order. The operations + * in this array do not overlap nor depend on each other. They may be + * safely reordered without any impact on the image that results from + * applying those operations. + */ + guac_display_plan_operation* ops; + + /** + * The number of operations stored in the ops array. + */ + size_t length; + + /** + * Index of operations in the plan by their image contents. Only operations + * that can be easily stored without collisions will be represented here. + */ + guac_display_plan_indexed_operation ops_by_hash[GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE]; + +} guac_display_plan; + +/** + * Creates a new guac_display_plan representing the changes necessary to + * transform the current remote display state seen by each connected user (the + * previous frame) to the current local display state represented by the + * guac_display (the current frame). The actual operations within the plan are + * chosen based on the result of passing the naive set of operations through an + * optimizer. + * + * There are cases where no plan will be generated. If no changes have occurred + * since the last frame, or if the last frame is still being encoded by the + * guac_display, NULL is returned. In the event that NULL is returned but + * changes have been made, those changes will eventually be automatically + * picked up after the currently-pending frame has finished encoded. + * + * The returned guac_display_plan must eventually be manually freed by a call + * to guac_display_plan_free(). + * + * IMPORTANT: The calling thread must already hold the write lock for the + * display's pending_frame.lock, and must at least hold the read lock for the + * display's last_frame.lock. + * + * @param display + * The guac_display to create a plan for. + * + * @return + * A newly-allocated guac_display_plan representing the changes necessary + * to transform the current remote display state to that of the local + * guac_display, or NULL if no plan could be created. If non-NULL, this + * value must eventually be freed by a call to guac_display_plan_free(). + */ +guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display); + +/** + * Frees all memory associated with the given guac_display_plan. + * + * @param plan + * The plan to free. + */ +void guac_display_plan_free(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * replacing draw operations with simple rects wherever draws consist only of a + * single color. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFR_guac_display_plan_rewrite_as_rects(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * storing the hashes of each outstanding draw operation within ops_by_hash. + * This function must be invoked before guac_display_plan_rewrite_as_copies() + * can be used for the current pending frame. + * + * @param plan + * The guac_display_plan to index. + */ +void PFR_guac_display_plan_index_dirty_cells(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * replacing draw operations with simple copies wherever draws can be rewritten + * as copies that pull image data from the previous frame. The display plan + * must first be indexed by guac_display_plan_index_dirty_cells() before this + * function can be used. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFR_LFR_guac_display_plan_rewrite_as_copies(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * combining horizontally-adjacent operations wherever doing so appears to be + * more efficient than performing those operations separately. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFW_guac_display_plan_combine_horizontally(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * combining vertically-adjacent operations wherever doing so appears to be + * more efficient than performing those operations separately. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFW_guac_display_plan_combine_vertically(guac_display_plan* plan); + +/** + * Enqueues all operations from the given plan within the operation FIFO used + * by the worker threads of the display associated with that plan. The + * display's worker threads will immediately begin picking up and performing + * these operations, with the final operation resulting in a frame boundary + * ("sync" instruction) being sent to connected users. + * + * @param plan + * The guac_display_plan to apply. + */ +void guac_display_plan_apply(guac_display_plan* plan); + +#endif diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h new file mode 100644 index 000000000..d66020fa7 --- /dev/null +++ b/src/libguac/display-priv.h @@ -0,0 +1,813 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_PRIV_H +#define GUAC_DISPLAY_PRIV_H + +#include "display-plan.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/rect.h" +#include "guacamole/socket.h" + +#include + +/** + * The maximum amount of time to wait after flushing a frame when compensating + * for client-side processing delays, in milliseconds. If a connected client is + * taking longer than this amount of additional time to process a received + * frame, processing lag compensation will be only partial (to avoid delaying + * further processing without bound for extremely slow clients). + */ +#define GUAC_DISPLAY_MAX_LAG_COMPENSATION 500 + +/* + * IMPORTANT: All functions defined within the internals of guac_display that + * DO NOT acquire locks on their own are given prefixes based on whether they + * access or modify the pending frame, last frame, or both. It is the + * responsibility of the caller of such functions to ensure that the required + * locks are either held or not relevant. + * + * The prefixes that may be added to function names are: + * + * "PFR_" + * The function reads (but does not write) the state of the pending frame. + * This prefix and "PFW_" are mutually-exclusive. + * + * "PFW_" + * The function writes (and possibly reads) the state of the pending frame. + * This prefix and "PFW_" are mutually-exclusive. + * + * "LFR_" + * The function reads (but does not write) the state of the last frame. + * This prefix and "LFW_" are mutually-exclusive. + * + * "LFW_" + * The function writes (and possibly reads) the state of the last frame. + * This prefix and "LFR_" are mutually-exclusive. + * + * "XFR_" + * The function reads (but does not write) the state of a frame, and + * whether that frame is the pending frame or the last frame depends on + * which frame is provided via function parameters. This prefix and "XFW_" + * are mutually-exclusive. + * + * "XFW_" + * The function writes (but does not read) the state of a frame, and + * whether that frame is the pending frame or the last frame depends on + * which frame is provided via function parameters. This prefix and "XFR_" + * are mutually-exclusive. + * + * Any functions lacking these prefixes either do not access last/pending + * frames in any way or take care of acquiring/releasing locks entirely on + * their own. + * + * These conventions are used for all functions in the internals of + * guac_display, not just those defined in this header. + */ + +/* + * IMPORTANT: In cases where a single thread must acquire multiple locks used + * by guac_display, proper acquisition order must be observed to avoid + * deadlock. The correct order is: + * + * 1) pending_frame.lock + * 2) last_frame.lock + * 3) ops + * 4) render_state + * + * Acquiring these locks in any other order risks deadlock. Don't do it. + */ + +/** + * The size of the image tiles (cells) that will be used to track changes to + * each layer, including gathering framerate statistics and performing indexing + * based on contents. Each side of each cell will consist of this many pixels. + * + * IMPORTANT: The hashing algorithm used to search the previous frame for + * content in the pending frame that has been reused (ie: scrolling) strongly + * depends on this value being 64. Any adjustment to this value will require + * corresponding and careful changes to the hashing algorithm. + */ +#define GUAC_DISPLAY_CELL_SIZE 64 + +/** + * The exponent of the power-of-two value that dictates the size of the image + * tiles (cells) that will be used to track changes to each layer + * (GUAC_DISPLAY_CELL_SIZE). + */ +#define GUAC_DISPLAY_CELL_SIZE_EXPONENT 6 + +/** + * The amount that the width/height of internal storage for graphical data + * should be rounded up to avoid unnecessary reallocations and copying. + */ +#define GUAC_DISPLAY_RESIZE_FACTOR 64 + +/** + * Given the width (or height) of a layer in pixels, calculates the width (or + * height) of that layer's pending_frame_cells array in cells. + * + * NOTE: It is not necessary to recalculate these values except when resizing a + * layer. In all other cases, the width/height of a layer in cells can be found + * in the pending_frame_cells_width and pending_frame_cells_height members + * respectively. + * + * @param pixels + * The width or height of the layer, in pixels. + * + * @return + * The width or height of that layer's pending_frame_cells array, in cells. + */ +#define GUAC_DISPLAY_CELL_DIMENSION(pixels) \ + ((pixels + GUAC_DISPLAY_CELL_SIZE - 1) / GUAC_DISPLAY_CELL_SIZE) + +/** + * The size of the operation FIFO read by the display worker threads. This + * value is the number of operation slots in the FIFO, not bytes. The amount of + * space currently specified here is roughly sufficient 8 worst-case frames + * worth of outstanding operations. + */ +#define GUAC_DISPLAY_WORKER_FIFO_SIZE ( \ + GUAC_DISPLAY_MAX_WIDTH * GUAC_DISPLAY_MAX_HEIGHT \ + / GUAC_DISPLAY_CELL_SIZE \ + / GUAC_DISPLAY_CELL_SIZE \ + * 8) + +/** + * Returns the memory address of the given rectangle within the mutable image + * buffer of the given guac_display_layer_state, where the upper-left corner of + * the given buffer is (0, 0). If the memory address cannot be calculated + * because doing so would overflow the maximum value of a size_t, execution of + * the current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param layer_state + * The guac_display_layer_state associated with the image buffer within + * which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + * + * @return + * The memory address of the given rectangle within the buffer of the given + * layer state. + */ +#define GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(layer_state, rect) \ + GUAC_RECT_MUTABLE_BUFFER(rect, (layer_state).buffer, (layer_state).buffer_stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +/** + * Returns the memory address of the given rectangle within the immutable + * (const) image buffer of the given guac_display_layer_state, where the + * upper-left corner of the given buffer is (0, 0). If the memory address + * cannot be calculated because doing so would overflow the maximum value of a + * size_t, execution of the current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param layer_state + * The guac_display_layer_state associated with the image buffer within + * which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + * + * @return + * The memory address of the given rectangle within the buffer of the given + * layer state. + */ +#define GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(layer_state, rect) \ + GUAC_RECT_CONST_BUFFER(rect, (layer_state).buffer, (layer_state).buffer_stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +/** + * Bitwise flag set on the render_state flag in guac_display when rendering of + * a pending frame is in progress (Guacamole instructions that draw the pending + * frame are being sent to connected users). + */ +#define GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS 1 + +/** + * Bitwise flag set on the render_state flag in guac_display when rendering of + * a pending frame is NOT in progress (Guacamole instructions that draw the + * pending frame are NOT being sent to connected users). + */ +#define GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS 2 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * the thread should be stopped. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING 1 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * visible, graphical changes have been made. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED 2 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * a frame boundary has been reached. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY 4 + +struct guac_display_render_thread { + + /** + * The display this render thread should render to. + */ + guac_display* display; + + /** + * The actual underlying POSIX thread. + */ + pthread_t thread; + + /** + * Flag representing render state. This flag is used to store whether the + * render thread is stopping and whether the current frame has been + * modified or is ready. + * + * @see GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + * @see GUAC_DISPLAY_RENDER_THREAD_FRAME_MODIFIED + * @see GUAC_DISPLAY_RENDER_THREAD_FRAME_READY + */ + guac_flag state; + + /** + * The number of frames that have been explicitly marked as ready since the + * last frame sent. This will be zero if explicit frame boundaries are not + * currently being used. + */ + unsigned int frames; + +}; + +/** + * Approximation of how often a region of a layer is modified, as well as what + * changes have been made to that region since the last frame. This information + * is used to help advise future optimizations, such as whether lossy + * compression is appropriate and whether parts of the layer can be copied from + * other regions rather than resend image data. + */ +typedef struct guac_display_layer_cell { + + /** + * The last time this particular cell was part of a frame (used to + * calculate framerate). + */ + guac_timestamp last_frame; + + /** + * The region of this cell that has been modified since the last frame was + * flushed. If the cell has not been modified at all, this will be an empty + * rect. + */ + guac_rect dirty; + + /** + * The rough number of pixels in the dirty rect that have been modified. If + * the cell has not been modified at all, this will be zero. + */ + size_t dirty_size; + + /** + * The display plan operation that is associated with this cell. If a + * display plan is not currently being created or optimized, this will be + * NULL. + */ + guac_display_plan_operation* related_op; + +} guac_display_layer_cell; + +/** + * The state of a Guacamole layer or buffer at some point in time. Within + * guac_display_layer, copies of this structure are used to represent the + * previous frame and the current, in-progress frame. The previous and + * in-progress frames are compared during flush to determine what graphical + * operations need to be sent to connected clients to efficiently transform the + * remote display from its previous state to the now-current state. + * + * IMPORTANT: The lock of the corresponding guac_display_state must be acquired + * before reading or modifying the values of any member of this structure. + */ +typedef struct guac_display_layer_state { + + /** + * The width of this layer in pixels. + */ + int width; + + /** + * The height of this layer in pixels. + */ + int height; + + /** + * The layer which contains this layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + const guac_layer* parent; + + /** + * The X coordinate of the upper-left corner of this layer, in pixels, + * relative to its parent layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + int x; + + /** + * The Y coordinate of the upper-left corner of this layer, in pixels, + * relative to its parent layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + int y; + + /** + * The Z-order of this layer, relative to sibling layers. This is only + * applicable to visible (non-buffer) layers which are not the default + * layer. + */ + int z; + + /** + * The level of opacity applied to this layer. Fully opaque is 255, while + * fully transparent is 0. This is only applicable to visible (non-buffer) + * layers which are not the default layer. + */ + int opacity; + + /** + * The number of simultaneous touches that this surface can accept, where 0 + * indicates that the surface does not support touch events at all. + */ + int touches; + + /** + * Non-zero if all graphical updates for this surface should use lossless + * compression, 0 otherwise. By default, newly-created surfaces will use + * lossy compression when heuristics determine it is appropriate. + */ + int lossless; + + /** + * The raw, 32-bit buffer of ARGB image data. If the layer was allocated as + * opaque, the alpha channel of each ARGB pixel will not be considered when + * compositing or when encoding images. + * + * So that large regions of image data can be easily compared, a consistent + * value for the alpha channel SHOULD be provided so that each 32-bit pixel + * can be compared without having to separately masking the channel. + * Optimizations within guac_display, including scroll detection, may + * assume that the alpha channel can always be considered when comparing + * pixel values for equivalence. + */ + unsigned char* buffer; + + /** + * The width of the image data, in pixels. This is not necessarily the same + * as the width of the layer. + */ + int buffer_width; + + /** + * The height of the image data, in pixels. This is not necessarily the + * same as the height of the layer. + */ + int buffer_height; + + /** + * The number of bytes in each row of image data. This is not necessarily + * equivalent to 4 * width. + */ + size_t buffer_stride; + + /** + * Non-zero if the image data referenced by the buffer pointer was + * allocated externally and should not be automatically freed or managed by + * guac_display, zero otherwise. + */ + int buffer_is_external; + + /** + * The approximate rectangular region containing all pixels within this + * layer that have been modified since the frame that occurred before this + * frame. If the layer was not modified, this will be an empty rect (zero + * width or zero height). + */ + guac_rect dirty; + + /** + * Whether this layer should be searched for possible scroll/copy + * optimizations. + */ + int search_for_copies; + + /* ---------------- LAYER LIST POINTERS ---------------- */ + + /** + * The layer immediately prior to this layer within the list containing + * this layer, or NULL if this is the first layer/buffer in the list. + */ + guac_display_layer* prev; + + /** + * The layer immediately following this layer within the list containing + * this layer, or NULL if this is the last layer/buffer in the list. + */ + guac_display_layer* next; + +} guac_display_layer_state; + +struct guac_display_layer { + + /** + * The guac_display instance that allocated this layer/buffer. + */ + guac_display* display; + + /** + * The Guacamole layer (or buffer) that this guac_display_layer will draw + * to when flushing a frame. + * + * NOTE: This value is set only during allocation and may safely be + * accessed without acquiring the overall layer lock. + */ + const guac_layer* layer; + + /** + * Whether the graphical data that will be written to this layer/buffer + * will only ever be opaque (no alpha channel). Compositing of graphical + * updates can be faster when no alpha channel need be considered. + */ + int opaque; + + /* ---------------- LAYER PREVIOUS FRAME STATE ---------------- */ + + /** + * The state of this layer when the last frame was flushed to connected clients. + * + * IMPORTANT: The display-level last_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_state last_frame; + + /** + * Off-screen buffer storing the contents of the previously-rendered frame + * for later use. If graphical updates are recognized as reusing data from + * a previous frame, that data will be copied from this buffer. Doing this + * simplifies the copy operation (there is no longer any need to perform + * those copies in a specific order) and ensures the copies are efficient + * on the client side (copying from one part of a graphical surface to + * another part of the same surface can be inefficient, particularly if the + * regions overlap). In practice, there is ample time between frames for + * the client to copy a layer's current contents to an off-screen buffer + * while awaiting the next frame. + * + * NOTE: This value is set only during allocation and may safely be + * accessed without acquiring the display-level last_frame.lock. + */ + guac_layer* last_frame_buffer; + + /* ---------------- LAYER PENDING FRAME STATE ---------------- */ + + /** + * The upcoming state of this layer when the current, in-progress frame is + * flushed to connected clients. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_state pending_frame; + + /** + * The Cairo context and surface containing the graphical data of the + * pending frame. The actual underlying buffer and details of the graphical + * surface are also available via pending_frame_raw_context. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_cairo_context pending_frame_cairo_context; + + /** + * The raw underlying buffer and details of the surface containing the + * graphical data of the pending frame. A Cairo context and surface backed + * by this buffer are also available via pending_frame_cairo_context. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_raw_context pending_frame_raw_context; + + /** + * A two-dimensional array of square tiles representing the nature of + * changes made to corresponding regions of the display. This is used both + * to track how frequently certain regions are being updated (to help + * inform whether lossy compression is appropriate), to track what parts of + * the frame have actually changed, and to aid in determining whether + * adjacent updated regions should be combined into a single update. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_cell* pending_frame_cells; + + /** + * The width of the pending_frame_cells array, in cells. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + size_t pending_frame_cells_width; + + /** + * The height of the pending_frame_cells array, in cells. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + size_t pending_frame_cells_height; + +}; + +typedef struct guac_display_state { + + /** + * Lock that guards concurrent access to any member of ANY STRUCTURE that + * relates to this guac_display_state, including the members of this + * structure. Unless explicitly documented otherwise, this lock MUST be + * acquired before accessing or modifying the members of this + * guac_display_state or any nested structure. + */ + guac_rwlock lock; + + /** + * The specific point in time that this guac_display_state represents. + */ + guac_timestamp timestamp; + + /** + * All layers and buffers that were part of the display at the time that + * the frame/snapshot represented by this guac_display_state was updated. + * + * NOTE: For each guac_display, there are two distinct lists of layers: the + * last frame layer list and the pending frame layer list: + * + * LAST FRAME LAYER LIST + * + * - HEAD: display->last_frame.layers + * - NEXT: layer->last_frame.next + * - PREV: layer->last_frame.prev + * + * PENDING LAYER LIST + * + * - HEAD: display->pending_frame.layers + * - NEXT: layer->pending_frame.next + * - PREV: layer->pending_frame.prev + * + * Existing layers are deleted only at the time a frame is flushed when a + * layer in the last frame layer list is found to no longer exist in the + * pending frame layer list. The same goes for the addition of new layers: + * they are added only during flush when a layer that was not present in + * the last frame layer list is found to be present in the pending frame + * layer list. + */ + guac_display_layer* layers; + + /** + * The X coordinate of the hotspot of the mouse cursor. The cursor image is + * stored/updated via the cursor_buffer member of guac_display. + */ + int cursor_hotspot_x; + + /** + * The Y coordinate of the hotspot of the mouse cursor. The cursor image is + * stored/updated via the cursor_buffer member of guac_display. + */ + int cursor_hotspot_y; + + /** + * The user that moved or clicked the mouse. This is used to ensure we + * don't attempt to synchronize an out-of-date mouse position to the user + * that is actively moving the mouse. + */ + guac_user* cursor_user; + + /** + * The X coordinate of the mouse cursor. + */ + int cursor_x; + + /** + * The Y coordinate of the mouse cursor. + */ + int cursor_y; + + /** + * The mask representing the states of all mouse buttons. + */ + int cursor_mask; + + /** + * The number of logical frames that have been rendered to this display + * state since the previous display state. + */ + unsigned int frames; + +} guac_display_state; + +struct guac_display { + + /* NOTE: Any member of this structure that requires protection against + * concurrent access is protected by its own lock. The overall display does + * not have nor need a top-level lock. */ + + /** + * The client associated with this display. + */ + guac_client* client; + + /* ---------------- DISPLAY FRAME STATES ---------------- */ + + /** + * The state of this display at the time the last frame was sent to + * connected users. + */ + guac_display_state last_frame; + + /** + * The pending state of this display that will become the next frame once + * it is sent to connected users. + */ + guac_display_state pending_frame; + + /** + * Whether the pending frame has been modified in any way outside of + * changing the mouse cursor or moving the mouse. This is used to help + * inform whether a frame should be flushed to update connected clients + * with respect to mouse cursor changes, or whether those changes can be + * safely assumed to be part of a larger frame containing general graphical + * updates. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + int pending_frame_dirty_excluding_mouse; + + /* ---------------- WELL-KNOWN LAYERS / BUFFERS ---------------- */ + + /** + * The default layer of the client display. + */ + guac_display_layer* default_layer; + + /** + * The buffer storing the current mouse cursor. The hotspot position within + * the cursor is stored within cursor_hotspot_x and cursor_hotspot_y of + * guac_display_state. + */ + guac_display_layer* cursor_buffer; + + /* ---------------- FRAME ENCODING WORKER THREADS ---------------- */ + + /** + * The number of worker threads in the worker_threads array. + */ + int worker_thread_count; + + /** + * Pool of worker threads that automatically pull from the ops FIFO, + * sending corresponding Guacamole instructions to all connected clients. + */ + pthread_t* worker_threads; + + /** + * FIFO of all graphical operations required to transform the remote + * display state from the previous frame to the next frame. Operations + * added to this FIFO will automatically be pulled and processed by a + * worker thread. + */ + guac_fifo ops; + + /** + * Storage for any items within the ops fifo. + */ + guac_display_plan_operation ops_items[GUAC_DISPLAY_WORKER_FIFO_SIZE]; + + /** + * The current number of active worker threads. + * + * IMPORTANT: This member must only be accessed or modified while the ops + * FIFO is locked. + */ + unsigned int active_workers; + + /** + * Whether least one pending frame has been deferred due to the encoding + * process being underway for a previous frame at the time it was + * completed. + * + * IMPORTANT: This member must only be accessed or modified while the ops + * FIFO is locked. + */ + int frame_deferred; + + /** + * The current state of the rendering process. Code that needs to be aware + * of whether a frame is currently in the process of being rendered can + * monitor the state of this flag, watching for either the + * GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS or + * GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS values. + */ + guac_flag render_state; + +}; + +/** + * Allocates and inserts a new element into the given linked list of display + * layers, associating it with the given layer and surface. + * + * @param head + * A pointer to the head pointer of the list of layers. The head pointer + * will be updated by this function to point to the newly-allocated + * display layer. + * + * @param layer + * The Guacamole layer to associated with the new display layer. + * + * @param opaque + * Non-zero if the new layer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * The newly-allocated display layer, which has been associated with the + * provided layer and surface. + */ +guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* layer, int opaque); + +/** + * Removes the given layer from all linked lists containing that layer and + * frees all associated memory. + * + * @param display_layer + * The layer to remove. + */ +void guac_display_remove_layer(guac_display_layer* display_layer); + +/** + * Resizes the given layer to the given dimensions, including any underlying + * image buffers. + * + * @param layer + * The layer to resize. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +void PFW_guac_display_layer_resize(guac_display_layer* layer, + int width, int height); + +/** + * Worker thread that continuously pulls operations from the operation FIFO of + * the given guac_display, applying those operations by seding corresponding + * instructions to connected clients. + * + * @param data + * A pointer to the guac_display. + * + * @return + * Always NULL. + */ +void* guac_display_worker_thread(void* data); + +#endif diff --git a/src/libguac/display-render-thread.c b/src/libguac/display-render-thread.c new file mode 100644 index 000000000..ac4a38892 --- /dev/null +++ b/src/libguac/display-render-thread.c @@ -0,0 +1,173 @@ +/* + * 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 "config.h" +#include "display-priv.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/flag.h" +#include "guacamole/mem.h" +#include "guacamole/timestamp.h" + +/** + * The maximum duration of a frame in milliseconds. This ensures we at least + * meet a reasonable minimum framerate in the case that the remote desktop + * server provides no frame boundaries and streams data continuously enough + * that frame boundaries are not discernable through timing. + * + * The current value of 100 is equivalent to 10 frames per second. + */ +#define GUAC_DISPLAY_RENDER_THREAD_MAX_FRAME_DURATION 100 + +/** + * The minimum duration of a frame in milliseconds. This ensures we don't start + * flushing a ton of tiny frames if a remote desktop server provides no frame + * boundaries and streams data inconsistently enough that timing would suggest + * frame boundaries in the middle of a frame. + * + * The current value of 10 is equivalent to 100 frames per second. + */ +#define GUAC_DISPLAY_RENDER_THREAD_MIN_FRAME_DURATION 10 + +/** + * The start routine for the display render thread, consisting of a single + * render loop. The render loop will proceed until signalled to stop, + * determining frame boundaries via a combination of heuristics and explicit + * marking (if available). + * + * @param data + * The guac_display_render_thread structure containing the render thread + * state. + * + * @return + * Always NULL. + */ +static void* guac_display_render_loop(void* data) { + + guac_display_render_thread* render_thread = (guac_display_render_thread*) data; + guac_display* display = render_thread->display; + + for (;;) { + + /* Wait indefinitely for any change to the frame state */ + guac_flag_wait_and_lock(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + + /* Bail out immediately upon upcoming disconnect */ + if (render_thread->state.value & GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING) { + guac_flag_unlock(&render_thread->state); + return NULL; + } + + int rendered_frames = 0; + + /* Lacking explicit frame boundaries, handle the change in frame state, + * continuing to accumulate frame modifications while still within + * heuristically determined frame boundaries */ + int allowed_wait = 0; + guac_timestamp frame_start = guac_timestamp_current(); + do { + + /* Continue processing messages for up to a reasonable + * minimum framerate without an explicit frame boundary + * indicating that the frame is not yet complete */ + int frame_duration = guac_timestamp_current() - frame_start; + if (frame_duration > GUAC_DISPLAY_RENDER_THREAD_MAX_FRAME_DURATION) { + guac_flag_unlock(&render_thread->state); + break; + } + + /* Use explicit frame boundaries whenever available */ + if (render_thread->state.value & GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY) { + + rendered_frames = render_thread->frames; + render_thread->frames = 0; + + guac_flag_clear(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + guac_flag_unlock(&render_thread->state); + break; + + } + + /* Do not exceed a reasonable maximum framerate without an + * explicit frame boundary terminating the frame early */ + allowed_wait = GUAC_DISPLAY_RENDER_THREAD_MIN_FRAME_DURATION - frame_duration; + if (allowed_wait < 0) + allowed_wait = 0; + + /* Wait for further modifications or other changes to frame state */ + + guac_flag_clear(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + guac_flag_unlock(&render_thread->state); + + } while (guac_flag_timedwait_and_lock(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED, allowed_wait)); + + guac_display_end_multiple_frames(display, rendered_frames); + + } + + return NULL; + +} + +guac_display_render_thread* guac_display_render_thread_create(guac_display* display) { + + guac_display_render_thread* render_thread = guac_mem_alloc(sizeof(guac_display_render_thread)); + + guac_flag_init(&render_thread->state); + render_thread->display = display; + render_thread->frames = 0; + + /* Start render thread (this will immediately begin blocking until frame + * modification or readiness is signalled) */ + pthread_create(&render_thread->thread, NULL, guac_display_render_loop, render_thread); + + return render_thread; + +} + +void guac_display_render_thread_notify_modified(guac_display_render_thread* render_thread) { + guac_flag_set(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); +} + +void guac_display_render_thread_notify_frame(guac_display_render_thread* render_thread) { + guac_flag_set_and_lock(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY); + render_thread->frames++; + guac_flag_unlock(&render_thread->state); +} + +void guac_display_render_thread_destroy(guac_display_render_thread* render_thread) { + + /* Clean up render thread after signalling it to stop */ + guac_flag_set(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING); + pthread_join(render_thread->thread, NULL); + + /* Free remaining resources */ + guac_flag_destroy(&render_thread->state); + guac_mem_free(render_thread); + +} + diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c new file mode 100644 index 000000000..39649f3f7 --- /dev/null +++ b/src/libguac/display-worker.c @@ -0,0 +1,522 @@ +/* + * 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/protocol-types.h" +#include "guacamole/protocol.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" + +#include +#include +#include +#include + +/** + * 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 */ + if (!display_layer->opaque) { + + 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); + + } + +} + +/** + * 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) { + + 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: + case GUAC_DISPLAY_PLAN_OPERATION_NOP: + 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 END_FRAME are handled during the initial, " + "single-threaded flush step. This is likely a bug.", + op.type); + break; + + case GUAC_DISPLAY_PLAN_END_FRAME: + + guac_fifo_lock(&display->ops); + int other_workers_busy = (display->active_workers > 1); + guac_fifo_unlock(&display->ops); + + /* If other workers are still busy, push the frame boundary + * back on the queue so that it's picked up by one of those + * workers */ + if (other_workers_busy) { + guac_fifo_enqueue(&display->ops, &op); + } + + /* Otherwise, we've reached the end of the frame, and this is + * the worker that will be sending that boundary to connected + * users */ + else { + + /* 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); + } + + /* Use the amount of time that the client has been waiting + * for a frame vs. the amount of time that it took the + * client to process the most recently acknowledged frame + * to calculate the amount of additional delay required to + * allow the client to catch up. This value is used later, + * after everything else related to the frame has been + * finalized. */ + int time_since_last_frame = guac_timestamp_current() - client->last_sent_timestamp; + int processing_lag = guac_client_get_processing_lag(client); + int required_wait = processing_lag - time_since_last_frame; + + /* 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 = ¤t->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; + + } + + /* Include an additional frame boundary to allow the client to also move forward with committing + * changes to the backing buffer while the server is receiving and preparing the next frame */ + guac_client_end_multiple_frames(client, 0); + + /* 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); + + /* Exclude local, server-side frame processing latency from + * waiting period */ + int latency = (int) (guac_timestamp_current() - display->last_frame.timestamp); + if (latency >= 0) { + guac_client_log(display->client, GUAC_LOG_TRACE, + "Rendering latency: %ims (%i:1 frame)\n", + latency, display->last_frame.frames); + required_wait -= latency; + } + + /* Ensure we don't wait without bound when compensating for + * client-side processing delays */ + if (required_wait > GUAC_DISPLAY_MAX_LAG_COMPENSATION) + required_wait = GUAC_DISPLAY_MAX_LAG_COMPENSATION; + + /* Allow connected clients to catch up if they're taking + * longer to process frames than the server is taking to + * generate them */ + if (required_wait > 0) { + guac_client_log(display->client, GUAC_LOG_TRACE, + "Waiting %ims to compensate for client-side " + "processing delays.\n", required_wait); + guac_timestamp_msleep(required_wait); + } + + guac_fifo_lock(&display->ops); + has_outstanding_frames = display->frame_deferred; + guac_fifo_unlock(&display->ops); + + } + + break; + + } + + guac_rwlock_release_lock(&display->last_frame.lock); + + guac_fifo_lock(&display->ops); + display->active_workers--; + guac_fifo_unlock(&display->ops); + + /* 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; + +} diff --git a/src/libguac/display.c b/src/libguac/display.c new file mode 100644 index 000000000..3e080e36b --- /dev/null +++ b/src/libguac/display.c @@ -0,0 +1,351 @@ +/* + * 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 "config.h" +#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/mem.h" +#include "guacamole/protocol.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" +#include "guacamole/user.h" + +#ifdef __MINGW32__ +#include +#endif + +#include +#include +#include +#include + +/** + * The number of worker threads to create per processor. + */ +#define GUAC_DISPLAY_CPU_THREAD_FACTOR 1 + +/** + * Returns the number of processors available to this process. If possible, + * limits on otherwise available processors like CPU affinity will be taken + * into account. If the number of available processors cannot be determined, + * zero is returned. + * + * @return + * The number of available processors, or zero if this value cannot be + * determined for any reason. + */ +static unsigned long guac_display_nproc() { + +#if defined(HAVE_SCHED_GETAFFINITY) + + /* Linux, etc. implementation leveraging sched_getaffinity() (this is + * specific to glibc and MUSL libc and is non-portable) */ + + cpu_set_t cpu_set; + CPU_ZERO(&cpu_set); + + if (sched_getaffinity(0, sizeof(cpu_set), &cpu_set) == 0) { + long cpu_count = CPU_COUNT(&cpu_set); + if (cpu_count > 0) + return cpu_count; + } + +#elif defined(_SC_NPROCESSORS_ONLN) + + /* Linux, etc. implementation leveraging sysconf() and _SC_NPROCESSORS_ONLN + * (which is also non-portable) */ + + long cpu_count = sysconf(_SC_NPROCESSORS_ONLN); + if (cpu_count > 0) + return cpu_count; + +#elif defined(__MINGW32__) + + /* Windows-specific implementation (clearly also non-portable) */ + + unsigned long cpu_count = 0; + DWORD_PTR process_mask, system_mask; + for (GetProcessAffinityMask(GetCurrentProcess(), &process_mask, &system_mask); + process_mask != 0; process_mask >>= 1) { + + if (process_mask & 1) + cpu_count++; + + } + + if (cpu_count > 0) + return cpu_count; + +#else + + /* Fallback implementation that does not query the number of CPUs available + * at all, returning an error code (as portable as it gets) */ + + long cpu_count = 0; + +#endif + + return 0; + +} + +guac_display* guac_display_alloc(guac_client* client) { + + /* Allocate and init core properties (really just the client pointer) */ + guac_display* display = guac_mem_zalloc(sizeof(guac_display)); + display->client = client; + + /* Init last frame and pending frame tracking */ + guac_rwlock_init(&display->last_frame.lock); + guac_rwlock_init(&display->pending_frame.lock); + display->last_frame.timestamp = display->pending_frame.timestamp = guac_timestamp_current(); + + /* It's safe to discard const of the default layer here, as + * guac_display_free_layer() function is specifically written to consider + * the default layer as const */ + display->default_layer = guac_display_add_layer(display, (guac_layer*) GUAC_DEFAULT_LAYER, 1); + display->cursor_buffer = guac_display_alloc_buffer(display, 0); + + /* Init operation FIFO used by worker threads */ + guac_fifo_init(&display->ops, display->ops_items, + GUAC_DISPLAY_WORKER_FIFO_SIZE, sizeof(guac_display_plan_operation)); + + /* Init flag used to notify threads that need to monitor whether a frame is + * currently being rendered */ + guac_flag_init(&display->render_state); + guac_flag_set(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + + int cpu_count = guac_display_nproc(); + if (cpu_count <= 0) { + guac_client_log(client, GUAC_LOG_WARNING, "Number of available " + "processors could not be determined. Assuming single-processor."); + cpu_count = 1; + } + else { + guac_client_log(client, GUAC_LOG_INFO, "Local system reports %i " + "processor(s) are available.", cpu_count); + } + + display->worker_thread_count = cpu_count * GUAC_DISPLAY_CPU_THREAD_FACTOR; + display->worker_threads = guac_mem_alloc(display->worker_thread_count, sizeof(pthread_t)); + guac_client_log(client, GUAC_LOG_INFO, "Graphical updates will be encoded " + "using %i worker thread(s).", display->worker_thread_count); + + /* Now that the core of the display has been fully initialized, it's safe + * to start the worker threads */ + for (int i = 0; i < display->worker_thread_count; i++) + pthread_create(&(display->worker_threads[i]), NULL, guac_display_worker_thread, display); + + return display; + +} + +void guac_display_free(guac_display* display) { + + /* Stop further use of the operation FIFO */ + guac_fifo_invalidate(&display->ops); + + /* Wait for all worker threads to terminate (they should nearly immediately + * terminate following invalidation of the FIFO) */ + for (int i = 0; i < display->worker_thread_count; i++) + pthread_join(display->worker_threads[i], NULL); + + /* All locks, FIFOs, etc. are now unused and can be safely destroyed */ + guac_flag_destroy(&display->render_state); + guac_fifo_destroy(&display->ops); + guac_rwlock_destroy(&display->last_frame.lock); + guac_rwlock_destroy(&display->pending_frame.lock); + + /* Free all layers within the pending_frame list (NOTE: This will also free + * those layers from the last_frame list) */ + while (display->pending_frame.layers != NULL) + guac_display_free_layer(display->pending_frame.layers); + + /* Free any remaining layers that were present only on the last_frame list + * and not on the pending_frame list */ + while (display->last_frame.layers != NULL) + guac_display_free_layer(display->last_frame.layers); + + guac_mem_free(display->worker_threads); + guac_mem_free(display); + +} + +void guac_display_dup(guac_display* display, guac_socket* socket) { + + guac_client* client = display->client; + guac_rwlock_acquire_read_lock(&display->last_frame.lock); + + /* Wait for any pending frame to finish being sent to established users of + * the connection before syncing any new users (doing otherwise could + * result in trailing instructions of that pending frame getting sent to + * new users after they finish joining, even though they are already in + * sync with that frame, and those trailing instructions may not have the + * intended meaning in context of the new users' remote displays) */ + guac_flag_wait_and_lock(&display->render_state, + GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + + /* Sync the state of all layers/buffers */ + guac_display_layer* current = display->last_frame.layers; + while (current != NULL) { + + const guac_layer* layer = current->layer; + + guac_rect layer_bounds; + guac_display_layer_get_bounds(current, &layer_bounds); + + int width = guac_rect_width(&layer_bounds); + int height = guac_rect_height(&layer_bounds); + guac_protocol_send_size(socket, layer, width, height); + + if (width > 0 && height > 0) { + + /* Get Cairo surface covering layer bounds */ + unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->last_frame, layer_bounds); + cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, + current->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + width, height, current->last_frame.buffer_stride); + + /* Send PNG for rect */ + guac_client_stream_png(client, socket, GUAC_COMP_OVER, layer, 0, 0, rect); + + /* Resync copy of previous frame */ + guac_protocol_send_copy(socket, + layer, 0, 0, width, height, + GUAC_COMP_OVER, current->last_frame_buffer, 0, 0); + + cairo_surface_destroy(rect); + + } + + /* Resync any properties that are specific to non-buffer layers */ + if (current->layer->index > 0) { + + /* Resync layer opacity */ + guac_protocol_send_shade(socket, current->layer, + current->last_frame.opacity); + + /* Resync layer position/hierarchy */ + guac_protocol_send_move(socket, current->layer, + current->last_frame.parent, + current->last_frame.x, + current->last_frame.y, + current->last_frame.z); + + } + + /* Resync multitouch support */ + if (current->layer->index >= 0) { + guac_protocol_send_set_int(socket, current->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, + current->last_frame.touches); + } + + current = current->last_frame.next; + + } + + /* Synchronize mouse cursor */ + guac_display_layer* cursor = display->cursor_buffer; + guac_protocol_send_cursor(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); + + /* Synchronize mouse location */ + guac_protocol_send_mouse(socket, display->last_frame.cursor_x, display->last_frame.cursor_y, + display->last_frame.cursor_mask, client->last_sent_timestamp); + + /* The initial frame synchronizing the newly-joined users is now complete */ + guac_protocol_send_sync(socket, client->last_sent_timestamp, display->last_frame.frames); + + /* Further rendering for the current connection can now safely continue */ + guac_flag_unlock(&display->render_state); + guac_rwlock_release_lock(&display->last_frame.lock); + + guac_socket_flush(socket); + +} + +void guac_display_notify_user_left(guac_display* display, guac_user* user) { + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Update to reflect leaving user, if necessary */ + if (display->pending_frame.cursor_user == user) + display->pending_frame.cursor_user = NULL; + + guac_rwlock_release_lock(&display->pending_frame.lock); +} + +void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user, int x, int y, int mask) { + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + display->pending_frame.cursor_user = user; + display->pending_frame.cursor_x = x; + display->pending_frame.cursor_y = y; + display->pending_frame.cursor_mask = mask; + guac_rwlock_release_lock(&display->pending_frame.lock); + + guac_display_end_mouse_frame(display); + +} + +guac_display_layer* guac_display_default_layer(guac_display* display) { + return display->default_layer; +} + +guac_display_layer* guac_display_alloc_layer(guac_display* display, int opaque) { + return guac_display_add_layer(display, guac_client_alloc_layer(display->client), opaque); +} + +guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque) { + return guac_display_add_layer(display, guac_client_alloc_buffer(display->client), opaque); +} + +void guac_display_free_layer(guac_display_layer* display_layer) { + + guac_display* display = display_layer->display; + const guac_layer* layer = display_layer->layer; + + guac_display_remove_layer(display_layer); + + if (layer->index != 0) { + + guac_client* client = display->client; + guac_protocol_send_dispose(client->socket, layer); + + /* As long as this isn't the display layer, it's safe to cast away the + * constness and free the underlying layer/buffer. Only the default + * layer (layer #0) is truly const. */ + if (layer->index > 0) + guac_client_free_layer(client, (guac_layer*) layer); + else + guac_client_free_buffer(client, (guac_layer*) layer); + + } + +} diff --git a/src/libguac/encode-webp.c b/src/libguac/encode-webp.c index 1314955e4..02411cddf 100644 --- a/src/libguac/encode-webp.c +++ b/src/libguac/encode-webp.c @@ -194,7 +194,7 @@ int guac_webp_write(guac_socket* socket, guac_stream* stream, /* Add additional tuning */ config.lossless = lossless; config.quality = quality; - config.thread_level = 1; /* Multi threaded */ + config.thread_level = 0; /* NOT multi-threaded (threading results in unnecessary overhead vs. the worker threads used by guac_display) */ config.method = 2; /* Compression method (0=fast/larger, 6=slow/smaller) */ /* Validate configuration */ diff --git a/src/libguac/fifo.c b/src/libguac/fifo.c new file mode 100644 index 000000000..30aef7c6e --- /dev/null +++ b/src/libguac/fifo.c @@ -0,0 +1,210 @@ +/* + * 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 "guacamole/fifo.h" +#include "guacamole/flag.h" + +#include +#include +#include + +void guac_fifo_init(guac_fifo* fifo, void* items, + size_t max_items, size_t item_size) { + + /* Init values describing the memory structure of the items array */ + fifo->items_offset = (char*) items - (char*) fifo; + fifo->max_items = max_items; + fifo->item_size = item_size; + + /* The fifo is currently empty */ + guac_flag_init(&fifo->state); + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_READY); + fifo->head = 0; + fifo->item_count = 0; + +} + +void guac_fifo_destroy(guac_fifo* fifo) { + guac_flag_destroy(&fifo->state); +} + +void guac_fifo_invalidate(guac_fifo* fifo) { + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_INVALID); +} + +void guac_fifo_lock(guac_fifo* fifo) { + guac_flag_lock(&fifo->state); +} + +void guac_fifo_unlock(guac_fifo* fifo) { + guac_flag_unlock(&fifo->state); +} + +int guac_fifo_is_valid(guac_fifo* fifo) { + /* We don't need to acquire the lock here as (1) we are only reading the + * flag and (2) the flag in question is a one-way, single-use signal (it's + * only set, never cleared) */ + return !(fifo->state.value & GUAC_FIFO_STATE_INVALID); +} + +int guac_fifo_enqueue(guac_fifo* fifo, + const void* item) { + + if (!guac_fifo_enqueue_and_lock(fifo, item)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_enqueue_and_lock(guac_fifo* fifo, + const void* item) { + + /* Block until fifo is ready for further items OR until the fifo is + * invalidated */ + guac_flag_wait_and_lock(&fifo->state, + GUAC_FIFO_STATE_INVALID | GUAC_FIFO_STATE_READY); + + /* Bail out if the fifo has become invalid */ + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + /* Abort program execution entirely if the fifo reports readiness but + * somehow actually does not have available space (this should never happen + * and indicates a bug) */ + if (fifo->item_count >= fifo->max_items) + abort(); + + /* Update count of items within the fifo, clearing the readiness flag if + * there is no longer any space for further items */ + fifo->item_count++; + if (fifo->item_count == fifo->max_items) + guac_flag_clear(&fifo->state, GUAC_FIFO_STATE_READY); + + /* NOTE: At this point, there are `item_count - 1` items present in the + * fifo, and `item_count - 1` is the index of the space in the items array + * that should receive the item being added (relative to head) */ + + /* Copy data of item buffer into last item in fifo */ + size_t tail = (fifo->head + fifo->item_count - 1) % fifo->max_items; + void* tail_item = ((char*) fifo) + fifo->items_offset + fifo->item_size * tail; + memcpy(tail_item, item, fifo->item_size); + + /* Advise any waiting threads that the fifo is now non-empty */ + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_NONEMPTY); + + /* Item enqueued successfully */ + return 1; + +} + +/** + * Dequeues a single item from the given guac_fifo, storing a copy + * of that item in the provided buffer. The event fifo MUST be non-empty. The + * state flag of the fifo MUST already be locked. + * + * @param fifo + * The guac_fifo to dequeue an item from. + * + * @param item + * The buffer that should receive a copy of the dequeued item. + */ +static void dequeue(guac_fifo* fifo, void* item) { + + /* Copy data of first item in fifo to provided output buffer */ + void* head_item = ((char*) fifo) + fifo->items_offset + fifo->item_size * fifo->head; + memcpy(item, head_item, fifo->item_size); + + /* Advance to next item in fifo, if any */ + fifo->item_count--; + fifo->head = (fifo->head + 1) % fifo->max_items; + + /* Keep state flag up-to-date with respect to non-emptiness ... */ + if (fifo->item_count == 0) + guac_flag_clear(&fifo->state, GUAC_FIFO_STATE_NONEMPTY); + + /* ... and readiness for further items */ + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_READY); + + /* Item has been dequeued successfully */ + +} + +int guac_fifo_dequeue(guac_fifo* fifo, void* item) { + + if (!guac_fifo_dequeue_and_lock(fifo, item)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_timed_dequeue(guac_fifo* fifo, + void* item, int msec_timeout) { + + if (!guac_fifo_timed_dequeue_and_lock(fifo, item, msec_timeout)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_dequeue_and_lock(guac_fifo* fifo, void* item) { + + /* Block indefinitely while waiting for an item to be added, but bail out + * if the fifo becomes invalid */ + guac_flag_wait_and_lock(&fifo->state, + GUAC_FIFO_STATE_NONEMPTY | GUAC_FIFO_STATE_INVALID); + + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + dequeue(fifo, item); + return 1; + +} + +int guac_fifo_timed_dequeue_and_lock(guac_fifo* fifo, + void* item, int msec_timeout) { + + /* Wait up to timeout for an item to be present in the fifo, failing if no + * items enter the fifo before the timeout lapses */ + if (!guac_flag_timedwait_and_lock(&fifo->state, + GUAC_FIFO_STATE_NONEMPTY | GUAC_FIFO_STATE_INVALID, + msec_timeout)) { + return 0; + } + + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + dequeue(fifo, item); + return 1; + +} + diff --git a/src/libguac/flag.c b/src/libguac/flag.c new file mode 100644 index 000000000..5f44ac31e --- /dev/null +++ b/src/libguac/flag.c @@ -0,0 +1,167 @@ +/* + * 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 "guacamole/flag.h" + +#include +#include +#include +#include +#include + +/** + * The number of nanoseconds in a whole second. + */ +#define NANOS_PER_SECOND 1000000000L + +void guac_flag_init(guac_flag* event_flag) { + + /* The condition used by guac_flag to signal changes in its + * value must be safe to share between processes, and must use the + * system-wide monotonic clock (not the realtime clock, which is subject to + * time changes) */ + pthread_condattr_t cond_attr; + pthread_condattr_init(&cond_attr); + pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC); + pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED); + pthread_cond_init(&event_flag->value_changed, &cond_attr); + + /* In addition to being safe to share between processes, the mutex used by + * guac_flag to guard concurrent access to its value (AND to + * signal changes in its value) must be recursive (you can lock the mutex + * again even if the current thread has already locked it) */ + pthread_mutexattr_t mutex_attr; + pthread_mutexattr_init(&mutex_attr); + pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED); + pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&event_flag->value_mutex, &mutex_attr); + + /* The initial value of all flags is unset (0) */ + event_flag->value = 0; + +} + +void guac_flag_destroy(guac_flag* event_flag) { + pthread_cond_destroy(&event_flag->value_changed); + pthread_mutex_destroy(&event_flag->value_mutex); +} + +void guac_flag_set_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Set specific bits of flag, leaving other bits unaffected */ + unsigned int old_value = event_flag->value; + event_flag->value |= flags; + + /* Signal other threads only if flag has changed as a result of this call */ + if (event_flag->value != old_value) + pthread_cond_broadcast(&event_flag->value_changed); + +} + +void guac_flag_set(guac_flag* event_flag, + unsigned int flags) { + guac_flag_set_and_lock(event_flag, flags); + guac_flag_unlock(event_flag); +} + +void guac_flag_clear_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Clear specific bits of flag, leaving other bits unaffected */ + event_flag->value &= ~flags; + + /* NOTE: Other threads are NOT signalled here. Threads wait only for flags + * to be set, not for flags to be cleared. */ + +} + +void guac_flag_clear(guac_flag* event_flag, + unsigned int flags) { + guac_flag_clear_and_lock(event_flag, flags); + guac_flag_unlock(event_flag); +} + +void guac_flag_lock(guac_flag* event_flag) { + pthread_mutex_lock(&event_flag->value_mutex); +} + +void guac_flag_unlock(guac_flag* event_flag) { + pthread_mutex_unlock(&event_flag->value_mutex); +} + +void guac_flag_wait_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Continue waiting until at least one of the desired flags has been set */ + while (!(event_flag->value & flags)) { + + /* Wait for any change to any flags, bailing out if something is wrong + * that would prevent waiting from ever succeeding (such a failure + * would turn this into a busy loop) */ + if (pthread_cond_wait(&event_flag->value_changed, + &event_flag->value_mutex)) { + abort(); /* This should not happen except due to a bug */ + } + + } + + /* If we reach this point, at least one of the desired flags has been set, + * and it is intentional that we continue to hold the lock (acquired on + * behalf of the caller) */ + +} + +int guac_flag_timedwait_and_lock(guac_flag* event_flag, + unsigned int flags, unsigned int msec_timeout) { + + guac_flag_lock(event_flag); + + struct timespec ts_timeout; + clock_gettime(CLOCK_MONOTONIC, &ts_timeout); + + uint64_t nsec_timeout = msec_timeout * 1000000 + ts_timeout.tv_nsec; + ts_timeout.tv_sec += nsec_timeout / NANOS_PER_SECOND; + ts_timeout.tv_nsec = nsec_timeout % NANOS_PER_SECOND; + + /* Continue waiting until at least one of the desired flags has been set */ + while (!(event_flag->value & flags)) { + + /* Wait for any change to any flags, failing if a timeout occurs */ + if (pthread_cond_timedwait(&event_flag->value_changed, + &event_flag->value_mutex, &ts_timeout)) { + guac_flag_unlock(event_flag); + return 0; + } + + } + + /* If we reach this point, at least one of the desired flags has been set, + * and it is intentional that we continue to hold the lock (acquired on + * behalf of the caller) */ + return 1; + +} + diff --git a/src/libguac/guacamole/assert.h b/src/libguac/guacamole/assert.h new file mode 100644 index 000000000..f8fd246ca --- /dev/null +++ b/src/libguac/guacamole/assert.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef GUAC_ASSERT_H +#define GUAC_ASSERT_H + +#include +#include + +/** + * Performs a runtime assertion that verifies the given condition evaluates to + * true (non-zero). If the condition evaluates to false (zero), execution is + * aborted with abort(). + * + * This macro should be used only in cases where the performance impact of + * verifying the assertion is negligible and it is benificial to always verify + * the assertion. Unlike the standard assert(), this macro will never be + * omitted by the compiler. + * + * @param expression + * The condition to test. + */ +#define GUAC_ASSERT(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "GUAC_ASSERT in %s() failed at %s:%i.\n", \ + __func__, __FILE__, __LINE__); \ + abort(); \ + } \ + } while(0) + +#endif diff --git a/src/libguac/guacamole/client-constants.h b/src/libguac/guacamole/client-constants.h index daf45b0f8..5683278dc 100644 --- a/src/libguac/guacamole/client-constants.h +++ b/src/libguac/guacamole/client-constants.h @@ -30,7 +30,7 @@ * The maximum number of inbound or outbound streams supported by any one * guac_client. */ -#define GUAC_CLIENT_MAX_STREAMS 64 +#define GUAC_CLIENT_MAX_STREAMS 512 /** * The index of a closed stream. diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index 8582389ee..01ecbc842 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -182,7 +182,8 @@ struct guac_client { /** * Lock which is acquired when the pending users list is being manipulated, - * or when the pending users list is being iterated. + * or iterated, or when checking/altering the + * __pending_users_thread_started flag. */ guac_rwlock __pending_users_lock; @@ -192,18 +193,14 @@ struct guac_client { * use within the client. This will be NULL until the first user joins * the connection, as it is lazily instantiated at that time. */ - timer_t __pending_users_timer; + pthread_t __pending_users_thread; /** - * A flag storing the current state of the pending users timer. + * Whether the pending users thread has started for this guac_client. The + * __pending_users_lock must be acquired before checking or altering this + * value. */ - int __pending_users_timer_state; - - /** - * A mutex that must be acquired before modifying or checking the value of - * the timer state. - */ - pthread_mutex_t __pending_users_timer_mutex; + int __pending_users_thread_started; /** * The first user within the list of connected users who have not yet had diff --git a/src/libguac/guacamole/display-constants.h b/src/libguac/guacamole/display-constants.h new file mode 100644 index 000000000..0f015a646 --- /dev/null +++ b/src/libguac/guacamole/display-constants.h @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_CONSTANTS_H +#define GUAC_DISPLAY_CONSTANTS_H + +/** + * @addtogroup display + * @{ + */ + +/** + * Provides constants related to the abstract display implementation + * (guac_display). + * + * @file display-constants.h + */ + +/** + * The maximum width of any guac_display, in pixels. + */ +#define GUAC_DISPLAY_MAX_WIDTH 8192 + +/** + * The maximum height of any guac_display, in pixels. + */ +#define GUAC_DISPLAY_MAX_HEIGHT 8192 + +/** + * The number of bytes in each pixel of raw image data within a + * guac_display_layer, as made accessible through a call to + * guac_display_layer_open_raw(). + */ +#define GUAC_DISPLAY_LAYER_RAW_BPP 4 + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/display-types.h b/src/libguac/guacamole/display-types.h new file mode 100644 index 000000000..61bbb4061 --- /dev/null +++ b/src/libguac/guacamole/display-types.h @@ -0,0 +1,106 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_TYPES_H +#define GUAC_DISPLAY_TYPES_H + +/** + * @addtogroup display + * @{ + */ + +/** + * Provides type definitions related to the abstract display implementation + * (guac_display). + * + * @file display-types.h + */ + +/** + * Opaque representation of the remote (client-side) display of a Guacamole + * connection (guac_client). + */ +typedef struct guac_display guac_display; + +/** + * Opaque representation of a thread that continuously renders updated + * graphical data to the remote display. + */ +typedef struct guac_display_render_thread guac_display_render_thread; + +/** + * Opaque representation of a layer within a guac_display. This may be a + * visible layer or an off-screen buffer, and is effectively the guac_display + * equivalent of a guac_layer. + */ +typedef struct guac_display_layer guac_display_layer; + +/** + * The current Cairo drawing context of a guac_display_layer, including a Cairo + * image surface wrapping the underlying drawing buffer of the + * guac_display_layer. After making graphical changes, the dirty rectangle of + * this context must be updated such that it includes the regions modified by + * those changes. + */ +typedef struct guac_display_layer_cairo_context guac_display_layer_cairo_context; + +/** + * The current raw drawing context of a guac_display_layer, including the + * underlying drawing buffer of the guac_display_layer and memory layout + * information. After making graphical changes, the dirty rectangle of this + * context must be updated such that it includes the regions modified by those + * changes. + */ +typedef struct guac_display_layer_raw_context guac_display_layer_raw_context; + +/** + * Pre-defined mouse cursor graphics. + */ +typedef enum guac_display_cursor_type { + + /** + * An empty (invisible/hidden) mouse cursor. + */ + GUAC_DISPLAY_CURSOR_NONE, + + /** + * A small dot. This is typically used in situations where cursor + * information for the remote desktop is not available, thus all cursor + * rendering must happen remotely, but it's still important that the user + * be able to see the current location of their local mouse pointer. + */ + GUAC_DISPLAY_CURSOR_DOT, + + /** + * A vertical, I-shaped bar indicating text input or selection. + */ + GUAC_DISPLAY_CURSOR_IBAR, + + /** + * A standard, general-purpose pointer. + */ + GUAC_DISPLAY_CURSOR_POINTER + +} guac_display_cursor_type; + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h new file mode 100644 index 000000000..84c4c9082 --- /dev/null +++ b/src/libguac/guacamole/display.h @@ -0,0 +1,780 @@ +/* + * 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. + */ + +#ifndef GUAC_DISPLAY_H +#define GUAC_DISPLAY_H + +/** + * An abstract display implementation which handles optimization automatically. + * Current optimizations include: + * + * - Scroll/copy detection + * - Solid color detection + * - Dirty rectangle reduction + * - Dynamic selection of PNG/JPEG/WebP compression depending on update content + * and frequency + * - Combining/rewriting of updates based on estimated cost + * + * @defgroup display guac_display + * @{ + */ + +/** + * Provides an abstract display implementation (guac_display), which handles + * optimization automatically. + * + * @file display.h + */ + +#include "client.h" +#include "display-constants.h" +#include "display-types.h" +#include "rect.h" +#include "socket.h" + +#include +#include + +/** + * Returns the memory address of the given rectangle within the image buffer of + * the given guac_display_layer_raw_context, where the upper-left corner of the + * given buffer is (0, 0). If the memory address cannot be calculated because + * doing so would overflow the maximum value of a size_t, execution of the + * current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param context + * The guac_display_layer_raw_context associated with the image buffer + * within which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + */ +#define GUAC_DISPLAY_LAYER_RAW_BUFFER(context, rect) \ + GUAC_RECT_MUTABLE_BUFFER(rect, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +struct guac_display_layer_cairo_context { + + /** + * A Cairo context created for the Cairo surface. This Cairo context is + * persistent and will maintain its state between different calls to + * guac_display_layer_open_cairo() for the same layer. + */ + cairo_t* cairo; + + /** + * A Cairo image surface wrapping the image buffer of this + * guac_display_layer. + */ + cairo_surface_t* surface; + + /** + * A rectangle covering the current bounds of the graphical surface. + */ + guac_rect bounds; + + /** + * A rectangle covering the region of the guac_display_layer that has + * changed since the last frame. This rectangle is initially empty and must + * be manually updated to cover any additional changed regions before + * closing the guac_display_layer_cairo_context. + */ + guac_rect dirty; + + /** + * The layer that should be searched for possible scroll/copy operations + * related to the changes being made via this guac_display_layer_cairo_context. + * This value is initially the layer being drawn to and must be updated + * before closing the context if a different source layer should be + * considered for scroll/copy optimizations. This value may be set to NULL + * to hint that no scroll/copy optimization should be performed. + */ + guac_display_layer* hint_from; + +}; + +struct guac_display_layer_raw_context { + + /** + * The raw, underlying image buffer of the guac_display_layer. If the layer + * was created as opaque, this image is 32-bit RGB with 8 bits per color + * component, where the lowest-order byte is the blue component and the + * highest-order byte is ignored. If the layer was not created as opaque, + * this image is 32-bit ARGB with 8 bits per color component, where the + * lowest-order byte is the blue component and the highest-order byte is + * alpha. + * + * This value may be replaced with a manually-allocated buffer if the + * associated layer should instead use that manually-allocated buffer for + * future rendering operations. If the buffer is replaced, it must be + * maintained manually going forward, including when the buffer needs to be + * resized or after the corresponding layer/display have been freed. + * + * If necessary (such as when a manually-allocated buffer must be freed + * before freeing the guac_display), all guac_display references to a + * manually-allocated buffer may be removed by setting this value to NULL + * and closing the context. Layers with a NULL buffer will not be + * considered for graphical changes in subsequent frames. + */ + unsigned char* buffer; + + /** + * The number of bytes in each row of image data. This value is not + * necessarily the same as the width of the image multiplied by the size of + * each pixel. Additional space may be allocated to allow for memory + * alignment or to make future resize operations more efficient. + * + * If the buffer for this layer is replaced with an external buffer, or if + * the external buffer changes structure, then this value must be manually + * kept up-to-date with the stride of the external buffer. + */ + size_t stride; + + /** + * A rectangle covering the current bounds of the graphical surface. The + * buffer must not be addressed outside these bounds. + * + * If the buffer for this layer is replaced with an external buffer, or if + * the external buffer changes size, then the dimensions of this bounds + * rect must be manually kept up-to-date with the dimensions of the + * external buffer. These dimensions will also be passed through to become + * the dimensions of the layer, since layers with external buffers cannot + * be resized with guac_display_layer_resize(). + * + * NOTE: If an external buffer is used and bounds dimensions are provided + * that are greater than GUAC_DISPLAY_MAX_WIDTH and + * GUAC_DISPLAY_MAX_HEIGHT, those values will instead be interpreted as + * equal to GUAC_DISPLAY_MAX_WIDTH and GUAC_DISPLAY_MAX_HEIGHT. + */ + guac_rect bounds; + + /** + * A rectangle covering the region of the guac_display_layer that has + * changed since the last frame. This rectangle is initially empty and must + * be manually updated to cover any additional changed regions before + * closing the guac_display_layer_raw_context. + */ + guac_rect dirty; + + /** + * The layer that should be searched for possible scroll/copy operations + * related to the changes being made via this guac_display_layer_raw_context. + * This value is initially the layer being drawn to and must be updated + * before closing the context if a different source layer should be + * considered for scroll/copy optimizations. This value may be set to NULL + * to hint that no scroll/copy optimization should be performed. + */ + guac_display_layer* hint_from; + +}; + +/** + * Allocates a new guac_display representing the remote display shared by all + * connected users of the given guac_client. The dimensions of the display + * should be set with guac_display_default_layer() and + * guac_display_layer_resize() once the desired display size is known. The + * guac_display must eventually be freed through a call to guac_display_free(). + * + * NOTE: If the buffer of a layer has been replaced with an externally + * maintained buffer, this function CANNOT be used to resize the layer. The + * layer must instead be resized through changing the bounds of a + * guac_display_layer_raw_context and, if necessary, replacing the underlying + * buffer again. + * + * @param client + * The guac_client whose remote display should be represented by the new + * guac_display. + * + * @return + * A newly-allocated guac_display representing the remote display of the + * given guac_client. + */ +guac_display* guac_display_alloc(guac_client* client); + +/** + * Frees all resources associated with the given guac_display. + * + * @param display + * The guac_display to free. + */ +void guac_display_free(guac_display* display); + +/** + * Replicates the current remote display state across the given socket. When + * new users join a particular guac_client, this function should be used to + * synchronize those users with the current display state. + * + * @param display + * The display that should be synchronized to all users at the other end of + * the given guac_socket. + * + * @param socket + * The socket to send the current remote display state over. + */ +void guac_display_dup(guac_display* display, guac_socket* socket); + +/** + * Notifies the given guac_display that a specific user has left the connection + * and need no longer be considered for future updates/events. This SHOULD + * always be called when a user leaves the connection to ensure other future, + * user-related events are interpreted correctly. + * + * @param display + * The guac_display to notify. + * + * @param user + * The user that left the connection. + */ +void guac_display_notify_user_left(guac_display* display, guac_user* user); + +/** + * Notifies the given guac_display that a specific user has changed the state + * of the mouse, such as through moving the pointer or pressing/releasing a + * mouse button. This function automatically invokes + * guac_display_end_mouse_frame(). + * + * @param display + * The guac_display to notify. + * + * @param user + * The user that moved the mouse or pressed/released a mouse button. + * + * @param x + * The X position of the mouse, in pixels. + * + * @param y + * The Y position of the mouse, in pixels. + * + * @param mask + * An integer value representing the current state of each button, where + * the Nth bit within the integer is set to 1 if and only if the Nth mouse + * button is currently pressed. The lowest-order bit is the left mouse + * button, followed by the middle button, right button, and finally the up + * and down buttons of the scroll wheel. + * + * @see GUAC_CLIENT_MOUSE_LEFT + * @see GUAC_CLIENT_MOUSE_MIDDLE + * @see GUAC_CLIENT_MOUSE_RIGHT + * @see GUAC_CLIENT_MOUSE_SCROLL_UP + * @see GUAC_CLIENT_MOUSE_SCROLL_DOWN + */ +void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user, int x, int y, int mask); + +/** + * Ends the current frame, where the number of input frames that were + * considered in creating this frame is either unknown or inapplicable, + * allowing the guac_display to complete sending the frame to connected + * clients. + * + * @param display + * The guac_display that should send the current frame. + */ +void guac_display_end_frame(guac_display* display); + +/** + * Ends the current frame only if the user-visible changes consist purely of + * updates to the mouse cursor position or icon. If other visible changes have + * been made, such as graphical updates to the display itself, this function + * has no effect. + * + * @param display + * The guac_display that should send the current frame if only the mouse + * cursor is visibly affected. + */ +void guac_display_end_mouse_frame(guac_display* display); + +/** + * Ends the current frame, where that frame may combine or otherwise represent the + * changes of an arbitrary number of input frames, allowing the guac_display to + * complete sending the frame to connected clients. + * + * @param display + * The guac_display that should send the current frame. + * + * @param + * The number of distinct frames that were considered or combined when + * generating the current frame, or zero if the boundaries of relevant + * frames are unknown. + */ +void guac_display_end_multiple_frames(guac_display* display, int frames); + +/** + * Returns the default layer for the given display. The default layer is the + * only layer that always exists and serves as the root-level layer for all + * other layers. + * + * @see GUAC_DEFAULT_LAYER + * + * @param display + * The guac_display to return the default layer from. + * + * @return + * A guac_display_layer representing the default layer for the given + * guac_display. + */ +guac_display_layer* guac_display_default_layer(guac_display* display); + +/** + * Allocates a new layer for the given display. The new layer will initially be + * a direct child of the display's default layer. When the layer is no longer + * needed, it may be freed through calling guac_display_free_layer(). If not + * freed manually through a call to guac_display_free_layer(), it will be freed + * when the display is freed with guac_display_free(). + * + * @param display + * The guac_display to allocate a new layer for. + * + * @param opaque + * Non-zero if the new layer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * A newly-allocated guac_display_layer that is initially a direct child of + * the default layer. + */ +guac_display_layer* guac_display_alloc_layer(guac_display* display, int opaque); + +/** + * Allocates a new buffer (offscreen layer) for the given display. When the + * buffer is no longer needed, it may be freed through calling + * guac_display_free_layer(). If not freed manually through a call to + * guac_display_free_layer(), it will be freed when the display is freed with + * guac_display_free(). + * + * @param display + * The guac_display to allocate a new buffer for. + * + * @param opaque + * Non-zero if the new buffer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * A newly-allocated guac_display_layer representing the new buffer. + */ +guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque); + +/** + * Frees the given layer, releasing any underlying memory. If the layer has + * already been used for rendering, it will be freed on the remote side, as + * well, when the current pending frame is complete. + * + * @param display_layer + * The layer to free. + */ +void guac_display_free_layer(guac_display_layer* display_layer); + +/** + * Returns a layer representing the current mouse cursor icon. Changes to the + * contents of this layer will affect the remote mouse cursor after the current + * pending frame is complete. + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to return the cursor layer for. + * + * @return + * A guac_display_layer representing the mouse cursor of the given + * guac_display. + */ +guac_display_layer* guac_display_cursor(guac_display* display); + +/** + * Sets the remote mouse cursor to the given built-in cursor icon. This + * function automatically invokes guac_display_end_mouse_frame(). + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to set the cursor of. + * + * @param cursor_type + * The built-in cursor icon to set the remote cursor to. + */ +void guac_display_set_cursor(guac_display* display, + guac_display_cursor_type cursor_type); + +/** + * Sets the hotspot location of the remote mouse cursor. The hotspot is the + * point within the mouse cursor where the click occurs. Changes to the hotspot + * of the remote mouse cursor will take effect after the current pending frame + * is complete. + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to set the cursor hotspot of. + * + * @param x + * The X coordinate of the cursor hotspot, in pixels. + * + * @param y + * The Y coordinate of the cursor hotspot, in pixels. + */ +void guac_display_set_cursor_hotspot(guac_display* display, int x, int y); + +/** + * Stores the current bounding rectangle of the given layer in the given + * guac_rect. The boundary stored will be the boundary of the current pending + * frame. + * + * @oaram layer + * The layer to determine the dimensions of. + * + * @param bounds + * The guac_rect that should receive the bounding rectangle of the given + * layer. + */ +void guac_display_layer_get_bounds(guac_display_layer* layer, guac_rect* bounds); + +/** + * Moves the given layer to the given coordinates. The changes to the given + * layer will be made as part of the current pending frame, and will not take + * effect on remote displays until the pending frame is complete. + * + * @param layer + * The layer to set the position of. + * + * @param x + * The X coordinate of the upper-left corner of the layer, in pixels. + * + * @param y + * The Y coordinate of the upper-left corner of the layer, in pixels. + */ +void guac_display_layer_move(guac_display_layer* layer, int x, int y); + +/** + * Sets the stacking position of the given layer relative to all other sibling + * layers (direct children of the same parent). The change in relative layer + * stacking position will be made as part of the current pending frame, and + * will not take effect on remote displays until the pending frame is complete. + * + * @param layer + * The layer to set the stacking position of. + * + * @param z + * The relative order of this layer. + */ +void guac_display_layer_stack(guac_display_layer* layer, int z); + +/** + * Reparents the given layer such that it is a direct child of the given parent + * layer. The change in layer hierarchy will be made as part of the current + * pending frame, and will not take effect on remote displays until the pending + * frame is complete. + * + * @param layer + * The layer to change the parent of. + * + * @param parent + * The layer that should be the new parent. + */ +void guac_display_layer_set_parent(guac_display_layer* layer, const guac_display_layer* parent); + +/** + * Sets the opacity of the given layer. The change in layer opacity will be + * made as part of the current pending frame, and will not take effect on + * remote displays until the pending frame is complete. + * + * @param layer + * The layer to change the opacity of. + * + * @param opacity + * The opacity to assign to the given layer, as a value between 0 and 255 + * inclusive, where 0 is completely transparent and 255 is completely + * opaque. + */ +void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity); + +/** + * Sets whether graphical changes to the given layer are allowed to be + * represented, updated, or sent using methods that can cause some loss of + * information, such as JPEG or WebP compression. By default, layers are + * allowed to use lossy methods. Changes to lossy vs. lossless behavior will + * affect the current pending frame, as well as any frames that follow. + * + * @param layer + * The layer to change the lossy behavior of. + * + * @param lossless + * Non-zero if the layer should be allowed to use lossy methods (the + * default behavior), zero if the layer should use strictly lossless + * methods. + */ +void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless); + +/** + * Sets the level of multitouch support available for the given layer. The + * change in layer multitouch support will be made as part of the current + * pending frame, and will not take effect on remote displays until the pending + * frame is complete. Setting multitouch support only has any effect on the + * default layer. + * + * @param layer + * The layer to set the multitouch support level of. + * + * @param touches + * The maximum number of simultaneous touches tracked by the layer, where 0 + * represents no touch support. + */ +void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches); + +/** + * Resizes the given layer to the given dimensions. The change in layer size + * will be made as part of the current pending frame, and will not take effect + * on remote displays until the pending frame is complete. + * + * This function will not resize the underlying buffer containing image data if + * the layer has been manually reassociated with a different, + * externally-maintained buffer using a guac_display_layer_raw_context. If this + * is the case, that buffer must instead be manually maintained by the caller, + * and resizing will typically involve replacing the buffer again. + * + * IMPORTANT: While it is safe to call this function while holding an open + * context (raw or Cairo), this should only be done if the underlying buffer is + * maintained externally or if the context is finished being used. Resizing a + * layer can result in the underlying buffer being replaced. + * + * @param layer + * The layer to set the size of. + * + * @param width + * The new width to assign to the layer, in pixels. Any values provided + * that are greater than GUAC_DISPLAY_MAX_WIDTH will instead be interpreted + * as equal to GUAC_DISPLAY_MAX_WIDTH. + * + * @param height + * The new height to assign to the layer, in pixels. Any values provided + * that are greater than GUAC_DISPLAY_MAX_HEIGHT will instead be + * interpreted as equal to GUAC_DISPLAY_MAX_HEIGHT. + */ +void guac_display_layer_resize(guac_display_layer* layer, int width, int height); + +/** + * Begins a drawing operation for the given layer, returning a context that can + * be used to draw directly to the raw image buffer containing the layer's + * current pending frame. + * + * Starting a draw operation acquires exclusive access to the display for the + * current thread. When complete, the original calling thread must relinquish + * exclusive access and free the graphical context by calling + * guac_display_layer_close_raw(). It is the responsibility of the caller to + * ensure the dirty rect within the returned context is updated to contain the + * region modified, such as by calling guac_rect_expand(). + * + * @param layer + * The layer to draw to. + * + * @return + * A mutable graphical context containing the current raw pending frame + * state of the given layer. + */ +guac_display_layer_raw_context* guac_display_layer_open_raw(guac_display_layer* layer); + +/** + * Ends a drawing operation that was started with a call to + * guac_display_layer_open_raw() and relinquishes exclusive access to the + * display. All graphical changes made to the layer through the raw context + * will be committed to the layer and will be included in the current pending + * frame. + * + * This function MUST NOT be called by any thread other than the thread that called + * guac_display_layer_open_raw() to obtain the given context. + * + * @param layer + * The layer that finished being drawn to. + * + * @param context + * The raw context of the drawing operation that has completed, as returned + * by a previous call to guac_display_layer_open_raw(). + */ +void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_raw_context* context); + +/** + * Fills a rectangle of image data within the given raw context with a single + * color. All pixels within the rectangle are replaced with the given color. If + * applicable, this includes the alpha channel. Compositing is not performed by + * this function. + * + * @param context + * The raw context of the layer that is being drawn to. + * + * @param dst + * The rectangular area that should be filled with the given color. + * + * @param color + * The color that should replace all current pixel values within the given + * rectangular region. + */ +void guac_display_layer_raw_context_set(guac_display_layer_raw_context* context, + const guac_rect* dst, uint32_t color); + +/** + * Copies a rectangle of image data from the given buffer to the given raw + * context, replacing all pixel values within the given rectangle. Compositing + * is not performed by this function. + * + * The size of the image data copied and the destination location of that data + * within the layer are dictated by the given rectangle. If any offset needs to + * be applied to the source image buffer, it is expected that this offset will + * already have been applied via the address of the buffer provided to this + * function, such as through an earlier call to GUAC_RECT_CONST_BUFFER(). + * + * @param context + * The raw context of the layer that is being drawn to. + * + * @param dst + * The rectangular area that should be filled with the image data from the + * given buffer. + * + * @param buffer + * The containing the image data that should replace all current pixel + * values within the given rectangular region. + * + * @param stride + * The number of bytes in each row of image data within the given buffer. + */ +void guac_display_layer_raw_context_put(guac_display_layer_raw_context* context, + const guac_rect* dst, const void* restrict buffer, size_t stride); + +/** + * Begins a drawing operation for the given layer, returning a context that can + * be used to draw to a Cairo surface containing the layer's current pending + * frame. The underlying Cairo state within the returned context will be + * preserved between calls to guac_display_layer_open_cairo(). + * + * Starting a draw operation acquires exclusive access to the display for the + * current thread. When complete, the original calling thread must relinquish + * exclusive access and free the graphical context by calling + * guac_display_layer_close_cairo(). It is the responsibility of the caller to + * ensure the dirty rect within the returned context is updated to contain the + * region modified, such as by calling guac_rect_expand(). + * + * @param layer + * The layer to draw to. + * + * @return + * A mutable graphical context containing the current pending frame state + * of the given layer in the form of a Cairo surface. + */ +guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_layer* layer); + +/** + * Ends a drawing operation that was started with a call to + * guac_display_layer_open_cairo() and relinquishes exclusive access to the + * display. All graphical changes made to the layer through the Cairo context + * will be committed to the layer and will be included in the current pending + * frame. + * + * This function MUST NOT be called by any thread other than the thread that called + * guac_display_layer_open_cairo() to obtain the given context. + * + * @param layer + * The layer that finished being drawn to. + * + * @param context + * The Cairo context of the drawing operation that has completed, as + * returned by a previous call to guac_display_layer_open_cairo(). + */ +void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_layer_cairo_context* context); + +/** + * Creates and starts a rendering thread for the given guac_display. The + * returned thread must eventually be freed with a call to + * guac_display_render_thread_destroy(). The rendering thread simplifies + * efficient handling of guac_display, but is not a requirement. If your use + * case is not well-served by the provided render thread, you can use your own + * render loop, thread, etc. + * + * The render thread will finalize and send frames after being notified that + * graphical changes have occurred, heuristically determining frame boundaries + * based on the lull in modifications that occurs between frames. In the event + * that modifications are made continuously without pause, the render thread + * will finalize and send frames at a reasonable minimum rate. + * + * If explicit frame boundaries are available, the render thread can be + * notified of these boundaries. Explicit boundaries will be preferred by the + * render thread over heuristically determined boundaries. + * + * @see guac_display_render_thread_notify_modified() + * @see guac_display_render_thread_notify_frame() + * + * @param display + * The display to start a rendering thread for. + * + * @return + * An opaque reference to the created, running rendering thread. This + * thread must be eventually freed through a call to + * guac_display_render_thread_destroy(). + */ +guac_display_render_thread* guac_display_render_thread_create(guac_display* display); + +/** + * Notifies the given render thread that the graphical state of the display has + * been modified in some visible way. The changes will then be included in a + * future frame by the render thread once a frame boundary has been reached. + * If frame boundaries are currently being determined heuristically by the + * render thread, it is the timing of calls to this function that determine the + * boundaries of frames. + * + * @param render_thread + * The render thread to notify of display modifications. + */ +void guac_display_render_thread_notify_modified(guac_display_render_thread* render_thread); + +/** + * Notifies the given render thread that a frame boundary has been reached. + * Further heuristic detection of frame boundaries by the render thread will + * stop, and all further frames must be marked through calls to this function. + * + * @param render_thread + * The render thread to notify of an explicit frame boundary. + */ +void guac_display_render_thread_notify_frame(guac_display_render_thread* render_thread); + +/** + * Safely stops and frees all resources associated with the given render + * thread. The provided pointer to the render thread is no longer valid after a + * call to this function. The guac_display associated with the render thread is + * unaffected. + * + * @param render_thread + * The render thread to stop and free. + */ +void guac_display_render_thread_destroy(guac_display_render_thread* render_thread); + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/fifo-constants.h b/src/libguac/guacamole/fifo-constants.h new file mode 100644 index 000000000..ff90d71c6 --- /dev/null +++ b/src/libguac/guacamole/fifo-constants.h @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#ifndef GUAC_FIFO_CONSTANTS_H +#define GUAC_FIFO_CONSTANTS_H + +/** + * @addtogroup fifo + * @{ + */ + +/** + * Provides constants for the abstract FIFO implementation (guac_fifo). + * + * @file fifo-constants.h + */ + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo has space for at least one item. + */ +#define GUAC_FIFO_STATE_READY 1 + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo contains at least one item. + */ +#define GUAC_FIFO_STATE_NONEMPTY 2 + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo is no longer valid and may not be used for any further operations. + */ +#define GUAC_FIFO_STATE_INVALID 4 + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/fifo-types.h b/src/libguac/guacamole/fifo-types.h new file mode 100644 index 000000000..7ab78b46b --- /dev/null +++ b/src/libguac/guacamole/fifo-types.h @@ -0,0 +1,53 @@ +/* + * 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. + */ + +#ifndef GUAC_FIFO_TYPES_H +#define GUAC_FIFO_TYPES_H + +/** + * @addtogroup fifo + * @{ + */ + +/** + * Provides type definitions for the abstract FIFO implementation (guac_fifo). + * + * @file fifo-types.h + */ + +/** + * Generic base structure for a FIFO of arbitrary events. The size of the FIFO + * and each event are up to the implementation. Each implementation must + * provide this base structure with a pointer to the underlying array of items, + * the maximum number of items supported, and the size in bytes of each item + * through a call to guac_fifo_init(). + * + * This generic base may be safely included in shared memory, but + * implementations building off this base must ensure the base is initialized + * with a call to guac_fifo_init() and that any additional + * implementation-specific aspects are also safe for shared memory usage. + */ +typedef struct guac_fifo guac_fifo; + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/fifo.h b/src/libguac/guacamole/fifo.h new file mode 100644 index 000000000..bfc3dda7c --- /dev/null +++ b/src/libguac/guacamole/fifo.h @@ -0,0 +1,314 @@ +/* + * 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. + */ + +#ifndef GUAC_FIFO_H +#define GUAC_FIFO_H + +#include "fifo-constants.h" +#include "fifo-types.h" +#include "flag.h" + +#include +#include + +/** + * Base FIFO implementation that allows arbitrary element sizes and arbitrary + * element storage. + * + * @defgroup fifo guac_fifo + * @{ + */ + +/** + * Provides an abstract FIFO implementation (guac_fifo), which can support + * arbitrary element sizes and storage. + * + * @file fifo.h + */ + +struct guac_fifo { + + /** + * The current state of this FIFO. This state primarily represents whether + * the FIFO contains at least one item (is non-empty), but it is also used + * to represent whether the FIFO is invalid (no longer permitted to contain + * any items). + */ + guac_flag state; + + /** + * The maximum number of items that may be stored in this FIFO. + */ + size_t max_items; + + /** + * The size of each individual item, in bytes. All FIFO items must have a + * constant size, though that size is implementation-dependent. + */ + size_t item_size; + + /** + * The index of the first item within this FIFO. As items are + * added/removed, this value will advance as necessary to avoid needing to + * spend CPU time moving existing items around in memory. + */ + size_t head; + + /** + * The current number of items stored within this FIFO. + */ + size_t item_count; + + /** + * The offset of the first byte of the implementation-specific array of + * items within this FIFO, relative to the first byte of guac_fifo + * structure. + */ + ssize_t items_offset; + +}; + +/** + * Initializes the given guac_fifo such that it may be safely included in + * shared memory and accessed by multiple processes. This function MUST be + * invoked once (and ONLY once) for each guac_fifo being used, and MUST be + * invoked before any such FIFO is used. + * + * The FIFO is empty upon initialization. + * + * @param fifo + * The FIFO to initialize. + * + * @param items + * The storage that the base implementation should use for queued items. + * This storage MUST be large enough to contain the maximum number of items + * as a contiguous array. + * + * @param max_items + * The maximum number of items supported by the provided storage. + * + * @param item_size + * The number of bytes required for each individual item in storage. + */ +void guac_fifo_init(guac_fifo* fifo, void* items, + size_t max_items, size_t item_size); + +/** + * Releases all underlying resources used by the given guac_fifo, such as + * pthread mutexes and conditions. The given guac_fifo MAY NOT be used after + * this function has been called. This function MAY NOT be called while + * exclusive access to the underlying state flag is held by any thread. + * + * This function does NOT free() the given guac_fifo pointer. If the memory + * associated with the given guac_fifo has been manually allocated, it must be + * manually freed as necessary. + * + * @param fifo + * The FIFO to destroy. + */ +void guac_fifo_destroy(guac_fifo* fifo); + +/** + * Marks the given FIFO as invalid, preventing any further additions or + * removals from the FIFO. Attempts to add/remove items from the FIFO from this + * point forward will fail immediately, as will any outstanding attempts to + * remove items that are currently blocked. + * + * This function is primarily necessary to allow for threadsafe cleanup of + * queues. Lacking this function, there is no guarantee that an outstanding + * call to guac_fifo_dequeue() won't still be indefinitely blocking. + * Internally, such a condition would mean that the mutex of the state flag is + * still held, which would mean that the FIFO can never be safely destroyed. + * + * @param fifo + * The FIFO to invalidate. + */ +void guac_fifo_invalidate(guac_fifo* fifo); + +/** + * Returns whether the given FIFO is still valid. A FIFO is valid if it has not + * yet been invalidated through a call to guac_fifo_invalidate(). + * + * @param fifo + * The FIFO to test. + * + * @return + * Non-zero if the given FIFO is still valid, zero otherwise. + */ +int guac_fifo_is_valid(guac_fifo* fifo); + +/** + * Acquires exclusive access to this guac_fifo. When exclusive access is no + * longer required, it must be manually relinquished through a call to + * guac_fifo_unlock(). This function may be safely called while the current + * thread already has exclusive access, however every such call must eventually + * have a matching call to guac_fifo_unlock(). + * + * NOTE: It is intended that locking/unlocking a guac_fifo may be used in lieu + * of a mutex to guard concurrent access to any number of shared resources + * related to the FIFO. + * + * @param fifo + * The guac_fifo to lock. + */ +void guac_fifo_lock(guac_fifo* fifo); + +/** + * Relinquishes exclusive access to this guac_fifo. This function may only be + * called by a thread that currently has exclusive access to the guac_fifo. + * + * NOTE: It is intended that locking/unlocking a guac_fifo may be used in lieu + * of a mutex to guard concurrent access to any number of shared resources + * related to the FIFO. + * + * @param fifo + * The guac_fifo to unlock. + */ +void guac_fifo_unlock(guac_fifo* fifo); + +/** + * Adds a copy of the given item to the end of the given FIFO, and signals any + * waiting threads that the FIFO is now non-empty. If there is insufficient + * space in the FIFO, this function will block until at space is available. If + * the FIFO is invalid or becomes invalid, this function returns immediately. + * + * @param fifo + * The FIFO to add an item to. + * + * @param item + * The item to add. + * + * @return + * Non-zero if the item was successfully added, zero if items cannot be + * added to the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_enqueue(guac_fifo* fifo, const void* item); + +/** + * Atomically adds a copy of the given item to the end of the given FIFO, + * signals any waiting threads that the FIFO is now non-empty, and leaves the + * given FIFO locked. If there is insufficient space in the FIFO, this function + * will block until at space is available. If the FIFO is invalid or becomes + * invalid, this function returns immediately and the FIFO is not locked. + * + * @param fifo + * The FIFO to add an item to. + * + * @param item + * The item to add. + * + * @return + * Non-zero if the item was successfully added, zero if items cannot be + * added to the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_enqueue_and_lock(guac_fifo* fifo, const void* item); + +/** + * Removes the oldest (first) item from the FIFO, storing a copy of that item + * within the provided buffer. If the FIFO is currently empty, this function + * will block until at least one item has been added to the FIFO or until the + * FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @return + * Non-zero if an item was successfully removed, zero if items cannot be + * removed from the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_dequeue(guac_fifo* fifo, void* item); + +/** + * Atomically removes the oldest (first) item from the FIFO, storing a copy of + * that item within the provided buffer. If this function successfully removes + * an item, the FIFO is left locked after this function returns. If the FIFO is + * currently empty, this function will block until at least one item has been + * added to the FIFO or until the FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @return + * Non-zero if an item was successfully removed, zero if items cannot be + * removed from the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_dequeue_and_lock(guac_fifo* fifo, void* item); + +/** + * Removes the oldest (first) item from the FIFO, storing a copy of that item + * within the provided buffer. If the FIFO is currently empty, this function + * will block until at least one item has been added to the FIFO, until the + * given timeout has elapsed, or until the FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one item to be + * present within the FIFO (or for the FIFO to become invalid). + * + * @return + * Non-zero if an item was successfully removed, zero if the timeout has + * elapsed or if items cannot be removed from the FIFO because the FIFO has + * been invalidated. + */ +int guac_fifo_timed_dequeue(guac_fifo* fifo, + void* item, int msec_timeout); + +/** + * Atomically removes the oldest (first) item from the FIFO, storing a copy of + * that item within the provided buffer. If this function successfully removes + * an item, the FIFO is left locked after this function returns. If the FIFO is + * currently empty, this function will block until at least one item has been + * added to the FIFO, until the given timeout has elapsed, or until the FIFO + * becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one item to be + * present within the FIFO (or for the FIFO to become invalid). + * + * @return + * Non-zero if an item was successfully removed, zero if the timeout has + * elapsed or if items cannot be removed from the FIFO because the FIFO has + * been invalidated. + */ +int guac_fifo_timed_dequeue_and_lock(guac_fifo* fifo, + void* item, int msec_timeout); + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/flag-types.h b/src/libguac/guacamole/flag-types.h new file mode 100644 index 000000000..e67fa5e9c --- /dev/null +++ b/src/libguac/guacamole/flag-types.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#ifndef GUAC_FLAG_TYPES_H +#define GUAC_FLAG_TYPES_H + +/** + * Generic integer flag intended for signalling of arbitrary events between + * processes. This flag may be safely included in shared memory, but must be + * initialized with guac_flag_init(). + * + * In addition to basic signalling and tracking of flag values, it is intended + * that the locking/unlocking facilities of guac_flag may be used in + * lieu of a mutex to guard concurrent access to any number of shared resources + * related to the flag. + */ +typedef struct guac_flag guac_flag; + +#endif + diff --git a/src/libguac/guacamole/flag.h b/src/libguac/guacamole/flag.h new file mode 100644 index 000000000..00f2f1aff --- /dev/null +++ b/src/libguac/guacamole/flag.h @@ -0,0 +1,244 @@ +/* + * 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. + */ + +#ifndef GUAC_FLAG_H +#define GUAC_FLAG_H + +#include "flag-types.h" + +#include + +struct guac_flag { + + /** + * The mutex used to ensure concurrent changes to the value of this flag + * are threadsafe, as well as to satisfy the requirements of the pthread + * conditional used to signal changes to the value of this flag. + */ + pthread_mutex_t value_mutex; + + /** + * Condition variable that signals when the value of this flag has changed. + */ + pthread_cond_t value_changed; + + /** + * The current value of this flag. This value may be the bitwise OR'd value + * of any number of arbitrary flags, so long as those flags fit within an + * int. It is entirely up to the user of this guac_flag to + * define the meaning of any value(s) assigned. + */ + unsigned int value; + +}; + +/** + * Initializes the given guac_flag such that it may be safely + * included in shared memory and accessed by multiple processes. This function + * MUST be invoked once (and ONLY once) for each guac_flag being + * used, and MUST be invoked before any such flag is used. + * + * The value of the flag upon initialization is 0 (no flags set). + * + * @param event_flag + * The flag to initialize. + */ +void guac_flag_init(guac_flag* event_flag); + +/** + * Releases all underlying resources used by the given guac_flag, + * such as pthread mutexes and conditions. The given guac_flag MAY + * NOT be used after this function has been called. This function MAY NOT be + * called while exclusive access to the guac_flag is held by any + * thread. + * + * This function does NOT free() the given guac_flag pointer. If the + * memory associated with the given guac_flag has been manually + * allocated, it must be manually freed as necessary. + * + * @param event_flag + * The flag to destroy. + */ +void guac_flag_destroy(guac_flag* event_flag); + +/** + * Sets the given bitwise flag(s) within the value of the given + * guac_flag, setting their corresponding bits to 1. The values of + * other bitwise flags are not affected. If other threads are waiting for any + * of these flags to be set, and at least one such flag has been set as a + * result of this call, they will be signalled accordingly. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be set. + */ +void guac_flag_set(guac_flag* event_flag, + unsigned int flags); + +/** + * Sets the given bitwise flag(s) within the value of the given guac_flag, + * setting their corresponding bits to 1, while also acquiring exclusive access + * to the guac_flag. The values of other bitwise flags are not affected. If + * other threads are waiting for any of these flags to be set, and at least one + * such flag has been set as a result of this call, they will be signalled + * accordingly. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be set. + */ +void guac_flag_set_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Clears the given bitwise flag(s) within the value of the given + * guac_flag, setting their corresponding bits to 0. The values of + * other bitwise flags are not affected. Unlike guac_flag_set(), + * no threads will be notified that these flag values have changed. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be cleared. Each bit in this + * value that is set to 1 will be set to 0 in the value of the + * guac_flag. + */ +void guac_flag_clear(guac_flag* event_flag, + unsigned int flags); + +/** + * Clears the given bitwise flag(s) within the value of the given guac_flag, + * setting their corresponding bits to 0, while also acquiring exclusive access + * to the guac_flag. The values of other bitwise flags are not affected. Unlike + * guac_flag_set(), no threads will be notified that these flag values have + * changed. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be cleared. Each bit in this + * value that is set to 1 will be set to 0 in the value of the + * guac_flag. + */ +void guac_flag_clear_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Acquires exclusive access to this guac_flag. When exclusive + * access is no longer required, it must be manually relinquished through a + * call to guac_flag_unlock(). This function may be safely called + * while the current thread already has exclusive access, however every such + * call must eventually have a matching call to guac_flag_unlock(). + * + * NOTE: It is intended that locking/unlocking a guac_flag may be + * used in lieu of a mutex to guard concurrent access to any number of shared + * resources related to the flag. + * + * @param event_flag + * The guac_flag to lock. + */ +void guac_flag_lock(guac_flag* event_flag); + +/** + * Relinquishes exclusive access to this guac_flag. This function + * may only be called by a thread that currently has exclusive access to the + * guac_flag. + * + * NOTE: It is intended that locking/unlocking a guac_flag may be + * used in lieu of a mutex to guard concurrent access to any number of shared + * resources related to the flag. + * + * @param event_flag + * The guac_flag to unlock. + */ +void guac_flag_unlock(guac_flag* event_flag); + +/** + * Waits indefinitely for any of the given flags to be set within the given + * guac_flag. This function returns only after at least one of the + * given flags has been set. After this function returns, the current thread + * has exclusive access to the guac_flag and MUST relinquish that + * access with a call to guac_flag_unlock() when finished. + * + * @param event_flag + * The guac_flag to wait on. + * + * @param flags + * The bitwise OR'd value of the specific flag(s) to wait for. + */ +void guac_flag_wait_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Waits no longer than the given number of milliseconds for any of the given + * flags to be set within the given guac_flag. This function returns + * after at least one of the given flags has been set, or after the provided + * time limit expires. After this function returns successfully, the current + * thread has exclusive access to the guac_flag and MUST relinquish + * that access with a call to guac_flag_unlock() when finished. If + * the time limit lapses before any of the given flags has been set, this + * function returns unsuccessfully without acquiring exclusive access. + * + * @param event_flag + * The guac_flag to wait on. + * + * @param flags + * The bitwise OR'd value of the specific flag(s) to wait for. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one of the + * desired flags to be set. + * + * @return + * Non-zero if at least one of the desired flags has been set and the + * current thread now has exclusive access to the guac_flag, zero if none + * of the desired flags were set within the time limit and the current + * thread DOES NOT have exclusive access. + */ +int guac_flag_timedwait_and_lock(guac_flag* event_flag, + unsigned int flags, unsigned int msec_timeout); + +#endif + diff --git a/src/libguac/guacamole/pool.h b/src/libguac/guacamole/pool.h index 0a250046a..4ff0877ee 100644 --- a/src/libguac/guacamole/pool.h +++ b/src/libguac/guacamole/pool.h @@ -102,8 +102,8 @@ void guac_pool_free(guac_pool* pool); /** * Returns the next available integer from the given guac_pool. All integers - * returned are non-negative, and are returned in sequences, starting from 0. - * This operation is threadsafe. + * returned are non-negative, and are returned in sequence, starting from 0. + * This operation is atomic. * * @param pool * The guac_pool to retrieve an integer from. @@ -111,14 +111,65 @@ void guac_pool_free(guac_pool* pool); * @return * The next available integer, which may be either an integer not yet * returned by a call to guac_pool_next_int, or an integer which was - * previously returned, but has since been freed. + * previously returned but has since been freed. */ int guac_pool_next_int(guac_pool* pool); +/** + * Returns the next available integer from the given guac_pool that is below + * the given limit. If no such integer can be obtained because all such + * integers are already in use, -1 will be returned instead. All integers + * successfully returned are non-negative, and are returned in sequence, + * starting from 0. This operation is atomic. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @param limit + * The exclusive upper bound to enforce on all integers returned by this + * function. Integers of this value or greater will never be returned. If + * all other integers are already in use, -1 will be returned instead. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. If all integers are + * currently in use and no integer can be returned without reaching the + * given limit, -1 is returned. + */ +int guac_pool_next_int_below(guac_pool* pool, int limit); + +/** + * Returns the next available integer from the given guac_pool that is below + * the given limit. If no such integer can be obtained because all such + * integers are already in use, the current process will abort and this + * function will not return. All integers successfully returned are + * non-negative, and are returned in sequence, starting from 0. This operation + * is atomic. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @param limit + * The exclusive upper bound to enforce on all integers returned by this + * function. Integers of this value or greater will never be returned. If + * all other integers are already in use, the current process will abort + * and this function will not return. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. If all integers are + * currently in use and no integer can be returned without reaching the + * given limit, the current process will abort and this function will not + * return. + */ +int guac_pool_next_int_below_or_die(guac_pool* pool, int limit); + /** * Frees the given integer back into the given guac_pool. The integer given * will be available for future calls to guac_pool_next_int. This operation is - * threadsafe. + * atomic. * * @param pool * The guac_pool to free the given integer into. diff --git a/src/libguac/guacamole/rect-types.h b/src/libguac/guacamole/rect-types.h new file mode 100644 index 000000000..73a1bdc93 --- /dev/null +++ b/src/libguac/guacamole/rect-types.h @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#ifndef GUAC_RECT_TYPES_H +#define GUAC_RECT_TYPES_H + +/** + * A rectangle defined by its upper-left and lower-right corners. The + * upper-left corner is inclusive (represents the start of the area contained + * by the guac_rect), while the lower-right corner is exclusive (represents the + * start of the area NOT contained by the guac_rect). All coordinates may be + * negative. + */ +typedef struct guac_rect guac_rect; + +#endif + diff --git a/src/libguac/guacamole/rect.h b/src/libguac/guacamole/rect.h new file mode 100644 index 000000000..477fec6bc --- /dev/null +++ b/src/libguac/guacamole/rect.h @@ -0,0 +1,271 @@ +/* + * 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. + */ + +#ifndef GUAC_RECT_H +#define GUAC_RECT_H + +#include "mem.h" +#include "rect-types.h" + +/** + * Returns the memory address of the given rectangle within the given mutable + * buffer, where the upper-left corner of the given buffer is (0, 0). If the + * memory address cannot be calculated because doing so would overflow the + * maximum value of a size_t, execution of the current process is automatically + * aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param rect + * The rectangle to determine the offset of. + * + * @param buffer + * The mutable buffer within which the address of the given rectangle + * should be determined. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param bpp + * The number of bytes in each pixel of image data. + * + * @return + * The memory address of the given rectangle within the given buffer. + */ +#define GUAC_RECT_MUTABLE_BUFFER(rect, buffer, stride, bpp) ((void*) ( \ + ((unsigned char*) (buffer)) \ + + guac_mem_ckd_mul_or_die((rect).top, stride) \ + + guac_mem_ckd_mul_or_die((rect).left, bpp))) + +/** + * Returns the memory address of the given rectangle within the given immutable + * (const) buffer, where the upper-left corner of the given buffer is (0, 0). + * If the memory address cannot be calculated because doing so would overflow + * the maximum value of a size_t, execution of the current process is + * automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param rect + * The rectangle to determine the offset of. + * + * @param buffer + * The const buffer within which the address of the given rectangle should + * be determined. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param bpp + * The number of bytes in each pixel of image data. + * + * @return + * The memory address of the given rectangle within the given buffer. + */ +#define GUAC_RECT_CONST_BUFFER(rect, buffer, stride, bpp) ((const void*) ( \ + ((const unsigned char*) (buffer)) \ + + guac_mem_ckd_mul_or_die((rect).top, stride) \ + + guac_mem_ckd_mul_or_die((rect).left, bpp))) + +struct guac_rect { + + /** + * The X coordinate of the upper-left corner of this rectangle (inclusive). + * This value represents the least integer X coordinate that is part of + * this rectangle, with greater integer X coordinates being part of this + * rectangle up to but excluding the right boundary. + * + * This value MUST be less than or equal to the right boundary. If this + * value is equal to the right boundary, the rectangle is empty (has no + * width). + */ + int left; + + /** + * The Y coordinate of the upper-left corner of this rectangle (inclusive). + * This value represents the least integer Y coordinate that is part of + * this rectangle, with greater integer Y coordinates being part of this + * rectangle up to but excluding the bottom boundary. + * + * This value MUST be less than or equal to the bottom boundary. If this + * value is equal to the bottom boundary, the rectangle is empty (has no + * height). + */ + int top; + + /** + * The X coordinate of the lower-right corner of this rectangle + * (exclusive). This value represents the least integer X coordinate that + * is NOT part of this rectangle, with lesser integer X coordinates being + * part of this rectangle up to and including the left boundary. + * + * This value MUST be greater than or equal to the left boundary. If this + * value is equal to the left boundary, the rectangle is empty (has no + * width). + */ + int right; + + /** + * The Y coordinate of the lower-right corner of this rectangle + * (exclusive). This value represents the least integer Y coordinate that + * is NOT part of this rectangle, with lesser integer Y coordinates being + * part of this rectangle up to and including the top boundary. + * + * This value MUST be greater than or equal to the top boundary. If this + * value is equal to the top boundary, the rectangle is empty (has no + * height). + */ + int bottom; + +}; + +/** + * Initializes the given rectangle with the given coordinates and dimensions. + * If a dimenion is negative, it is interpreted as if zero. + * + * @param rect + * The rectangle to initialize. + * + * @param x + * The X coordinate of the upper-left corner of the rectangle. + * + * @param y + * The Y coordinate of the upper-left corner of the rectangle. + * + * @param width + * The width of the rectangle. + * + * @param height + * The height of the rectangle. + */ +void guac_rect_init(guac_rect* rect, int x, int y, int width, int height); + +/** + * Extends the given rectangle such that each edge of the rectangle falls on + * the edge of an NxN cell in a regular grid anchored at the upper-left corner, + * where N is a power of two. + * + * @param rect + * The rectangle to adjust. + * + * @param bits + * The size of the cells in the grid, as the exponent of the power of two + * size of each grid cell edge. For example, to align the given rectangle + * to the edges of a grid containing 8x8 cells, use a value of 3. + */ +void guac_rect_align(guac_rect* rect, unsigned int bits); + +/** + * Extends the given rectangle such that it contains at least the specified + * minimum rectangle. + * + * @param rect + * The rectangle to extend. + * + * @param min + * The minimum area which must be contained within the given rectangle. + */ +void guac_rect_extend(guac_rect* rect, const guac_rect* min); + +/** + * Collapses the given rectangle such that it exists only within the bounds of + * the given maximum rectangle. + * + * @param rect + * The rectangle to collapse. + * + * @param max + * The maximum area in which the given rectangle can exist. + */ +void guac_rect_constrain(guac_rect* rect, const guac_rect* max); + +/** + * Reduces the size of the given rectangle such that it does not exceed the + * given width and height. The aspect ratio of the given rectangle is + * preserved. If the original rectangle is already smaller than the given width + * and height, this function has no effect. + * + * @param rect + * The rectangle to shrink while preserving aspect ratio. + * + * @param max_width + * The maximum width that the given rectangle may have. + * + * @param max_height + * The maximum height that the given rectangle may have. + */ +void guac_rect_shrink(guac_rect* rect, int max_width, int max_height); + +/** + * Returns whether the two given rectangles intersect. + * + * @param a + * One of the rectangles to check. + * + * @param b + * The other rectangle to check. + * + * @return + * Non-zero if the rectangles intersect, zero otherwise. + */ +int guac_rect_intersects(const guac_rect* a, const guac_rect* b); + +/** + * Returns whether the given rectangle is empty. A rectangle is empty if it has + * no area (has an effective width or height of zero). + * + * @param rect + * The rectangle to test. + * + * @return + * Non-zero if the rectangle is empty, zero otherwise. + */ +int guac_rect_is_empty(const guac_rect* rect); + +/** + * Returns the width of the given rectangle. + * + * @param rect + * The rectangle to determine the width of. + * + * @return + * The width of the given rectangle. + */ +int guac_rect_width(const guac_rect* rect); + +/** + * Returns the height of the given rectangle. + * + * @param rect + * The rectangle to determine the height of. + * + * @return + * The height of the given rectangle. + */ +int guac_rect_height(const guac_rect* rect); + +#endif diff --git a/src/libguac/guacamole/user-constants.h b/src/libguac/guacamole/user-constants.h index 0da48c0bb..40832c16c 100644 --- a/src/libguac/guacamole/user-constants.h +++ b/src/libguac/guacamole/user-constants.h @@ -35,7 +35,7 @@ * The maximum number of inbound or outbound streams supported by any one * guac_user. */ -#define GUAC_USER_MAX_STREAMS 64 +#define GUAC_USER_MAX_STREAMS 512 /** * The index of a closed stream. diff --git a/src/libguac/mem.c b/src/libguac/mem.c index 8f9a842a6..06735bf00 100644 --- a/src/libguac/mem.c +++ b/src/libguac/mem.c @@ -17,6 +17,7 @@ * under the License. */ +#include "guacamole/assert.h" #include "guacamole/error.h" #include "guacamole/mem.h" #include "guacamole/private/mem.h" @@ -126,8 +127,7 @@ size_t PRIV_guac_mem_ckd_mul_or_die(size_t factor_count, const size_t* factors) /* Perform request multiplication, aborting the entire process if the * calculation overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_mul(&result, factor_count, factors)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_mul(&result, factor_count, factors)); return result; @@ -138,8 +138,7 @@ size_t PRIV_guac_mem_ckd_add_or_die(size_t term_count, const size_t* terms) { /* Perform request addition, aborting the entire process if the calculation * overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_add(&result, term_count, terms)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_add(&result, term_count, terms)); return result; @@ -150,8 +149,7 @@ size_t PRIV_guac_mem_ckd_sub_or_die(size_t term_count, const size_t* terms) { /* Perform request subtraction, aborting the entire process if the * calculation overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_sub(&result, term_count, terms)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_sub(&result, term_count, terms)); return result; @@ -238,8 +236,7 @@ void* PRIV_guac_mem_realloc_or_die(void* mem, size_t factor_count, const size_t* /* Perform requested resize, aborting the entire process if this cannot be * done */ void* resized_mem = PRIV_guac_mem_realloc(mem, factor_count, factors); - if (resized_mem == NULL && guac_error != GUAC_STATUS_SUCCESS) - abort(); + GUAC_ASSERT(resized_mem != NULL || guac_error == GUAC_STATUS_SUCCESS); return resized_mem; diff --git a/src/libguac/pool.c b/src/libguac/pool.c index b22c20934..291129183 100644 --- a/src/libguac/pool.c +++ b/src/libguac/pool.c @@ -19,9 +19,11 @@ #include "config.h" +#include "guacamole/assert.h" #include "guacamole/mem.h" #include "guacamole/pool.h" +#include #include guac_pool* guac_pool_alloc(int size) { @@ -69,42 +71,115 @@ void guac_pool_free(guac_pool* pool) { } -int guac_pool_next_int(guac_pool* pool) { +/** + * Returns the next available integer from the given guac_pool. All integers + * returned are non-negative, and are returned in sequence, starting from 0. + * + * Unlike the public guac_pool_next_int() function, this function is NOT atomic + * and depends on the caller having already acquired the pool's lock. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. + */ +static int __guac_pool_next_int(guac_pool* pool) { int value; - /* Acquire exclusive access */ - pthread_mutex_lock(&(pool->__lock)); + /* It's unlikely that any usage of guac_pool will ever manage to reach + * INT_MAX concurrent requests for integers, but we definitely should bail + * out if ever this does happen. Tracing this sort of issue down would be + * extremely difficult without fail-fast behavior. */ + GUAC_ASSERT(pool->__next_value < INT_MAX); + GUAC_ASSERT(pool->active < INT_MAX); pool->active++; /* If more integers are needed, return a new one. */ - if (pool->__head == NULL || pool->__next_value < pool->min_size) { + if (pool->__head == NULL || pool->__next_value < pool->min_size) value = pool->__next_value++; - pthread_mutex_unlock(&(pool->__lock)); - return value; + + /* Otherwise, reuse a previously freed integer */ + else { + + value = pool->__head->value; + + /* If only one element exists, reset pool to empty. */ + if (pool->__tail == pool->__head) { + guac_mem_free(pool->__head); + pool->__head = NULL; + pool->__tail = NULL; + } + + /* Otherwise, advance head. */ + else { + guac_pool_int* old_head = pool->__head; + pool->__head = old_head->__next; + guac_mem_free(old_head); + } + } - /* Otherwise, remove first integer. */ - value = pool->__head->value; + /* Again, this should never happen and would be a sign of some fairly + * fundamental assumption failing. It's important for such things to fail + * fast. */ + GUAC_ASSERT(value >= 0); + + return value; + +} + +int guac_pool_next_int(guac_pool* pool) { + + pthread_mutex_lock(&(pool->__lock)); + int value = __guac_pool_next_int(pool); + pthread_mutex_unlock(&(pool->__lock)); + + return value; + +} + +int guac_pool_next_int_below(guac_pool* pool, int limit) { + + pthread_mutex_lock(&(pool->__lock)); + + int value; - /* If only one element exists, reset pool to empty. */ - if (pool->__tail == pool->__head) { - guac_mem_free(pool->__head); - pool->__head = NULL; - pool->__tail = NULL; + /* Explicitly bail out now if there we would need to return a new integer, + * but can't without reaching the given limit */ + if (pool->active >= limit || (pool->__next_value >= limit && pool->__head == NULL)) { + value = -1; } - /* Otherwise, advance head. */ + /* In all other cases, attempt to obtain the requested integer (either + * reusing a freed integer or allocating a new one), but verify that some + * fundamental misuse of guac_pool hasn't resulted in values defying + * expectations */ else { - guac_pool_int* old_head = pool->__head; - pool->__head = old_head->__next; - guac_mem_free(old_head); + value = __guac_pool_next_int(pool); + GUAC_ASSERT(value < limit); } - /* Return retrieved value. */ pthread_mutex_unlock(&(pool->__lock)); + + return value; + +} + +int guac_pool_next_int_below_or_die(guac_pool* pool, int limit) { + + int value = guac_pool_next_int_below(pool, limit); + + /* Abort current process entirely if no integer can be obtained without + * reaching the given limit */ + GUAC_ASSERT(value >= 0); + return value; + } void guac_pool_free_int(guac_pool* pool, int value) { @@ -117,6 +192,7 @@ void guac_pool_free_int(guac_pool* pool, int value) { /* Acquire exclusive access */ pthread_mutex_lock(&(pool->__lock)); + GUAC_ASSERT(pool->active > 0); pool->active--; /* If pool empty, store as sole entry. */ diff --git a/src/libguac/rect.c b/src/libguac/rect.c new file mode 100644 index 000000000..2d372fe67 --- /dev/null +++ b/src/libguac/rect.c @@ -0,0 +1,178 @@ +/* + * 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 "guacamole/rect.h" + +/** + * Given a bitmask that is one less than a power of two (ie: 0xF, 0x1F, etc.), + * rounds the given value in the negative direction to the nearest multiple of + * that power of two. Positive values are rounded down towards zero while + * negative values are rounded up toward negative values of greater magnitude. + * + * @param value + * The value to round. + * + * @param mask + * A bitmask whose integer value is one less than a power of two. + * + * @return + * The given value, rounded to the nearest multiple of the power of two + * represented by the given mask, where that rounding is performed in the + * negative direction. + */ +#define GUAC_RECT_ROUND_NEG(value, mask) (value & ~mask) + +/** + * Given a bitmask that is one less than a power of two (ie: 0xF, 0x1F, etc.), + * rounds the given value in the positive direction to the nearest multiple of + * that power of two. Negative values are rounded down towards zero while + * positive values are rounded up toward positive values of greater magnitude. + * + * @param value + * The value to round. + * + * @param mask + * A bitmask whose integer value is one less than a power of two. + * + * @return + * The given value, rounded to the nearest multiple of the power of two + * represented by the given mask, where that rounding is performed in the + * positive direction. + */ +#define GUAC_RECT_ROUND_POS(value, mask) ((value + mask) & ~mask) + +void guac_rect_init(guac_rect* rect, int x, int y, int width, int height) { + *rect = (guac_rect) { + .left = x, + .top = y, + .right = width > 0 ? x + width : x, + .bottom = height > 0 ? y + height : y + }; +} + +void guac_rect_extend(guac_rect* rect, const guac_rect* min) { + + /* The union of an empty rect and the provided rect should be that provided + * rect. Considering the garbage coordinates that may be present in an + * empty rect can otherwise produce incorrect results. */ + if (guac_rect_is_empty(rect)) { + *rect = *min; + return; + } + + /* Extend edges of rectangle such that it contains the provided minimum + * rectangle */ + if (min->left < rect->left) rect->left = min->left; + if (min->top < rect->top) rect->top = min->top; + if (min->right > rect->right) rect->right = min->right; + if (min->bottom > rect->bottom) rect->bottom = min->bottom; + +} + +void guac_rect_constrain(guac_rect* rect, const guac_rect* max) { + + /* Shrink edges of rectangle such that it is contained by the provided + * maximum rectangle */ + if (max->left > rect->left) rect->left = max->left; + if (max->top > rect->top) rect->top = max->top; + if (max->right < rect->right) rect->right = max->right; + if (max->bottom < rect->bottom) rect->bottom = max->bottom; + +} + +void guac_rect_shrink(guac_rect* rect, int max_width, int max_height) { + + int original_width = guac_rect_width(rect); + int original_height = guac_rect_height(rect); + + /* Shrink only; do not _expand_ to reach the max width/height */ + if (original_width < max_width) max_width = original_width; + if (original_height < max_height) max_height = original_height; + + /* BOTH the width and height must be adjusted by the same factor in + * order to preserve aspect ratio. Choosing the smallest adjustment + * factor guarantees that the rectangle will be within bounds while + * preserving aspect ratio to the greatest degree possible (there + * is unavoidable integer rounding error). */ + + int scale_numerator, scale_denominator; + + /* NOTE: The following test is mathematically equivalent to: + * + * if (max_width / original_width < max_height / original_height) { + * ... + * } + * + * but does not require floating point arithmetic. */ + if (max_width * original_height < max_height * original_width) { + scale_numerator = max_width; + scale_denominator = original_width; + } + else { + scale_numerator = max_height; + scale_denominator = original_height; + } + + rect->right = rect->left + original_width * scale_numerator / scale_denominator; + rect->bottom = rect->top + original_height * scale_numerator / scale_denominator; + +} + + +void guac_rect_align(guac_rect* rect, unsigned int bits) { + + if (bits == 0) + return; + + int factor = 1 << bits; + int mask = factor - 1; + + /* Expand and shift rectangle as necessary for its edges to be aligned + * along multiples of the given power of two */ + rect->left = GUAC_RECT_ROUND_NEG(rect->left, mask); + rect->top = GUAC_RECT_ROUND_NEG(rect->top, mask); + rect->right = GUAC_RECT_ROUND_POS(rect->right, mask); + rect->bottom = GUAC_RECT_ROUND_POS(rect->bottom, mask); + +} + +int guac_rect_intersects(const guac_rect* a, const guac_rect* b) { + + /* Two rectangles intersect if neither rectangle is wholly outside the + * other */ + return !( + b->right <= a->left || a->right <= b->left + || b->bottom <= a->top || a->bottom <= b->top + ); + +} + +int guac_rect_is_empty(const guac_rect* rect) { + return rect->right <= rect->left || rect->bottom <= rect->top; +} + +int guac_rect_width(const guac_rect* rect) { + int width = rect->right - rect->left; + return width > 0 ? width : 0; +} + +int guac_rect_height(const guac_rect* rect) { + int height = rect->bottom - rect->top; + return height > 0 ? height : 0; +} diff --git a/src/libguac/tests/Makefile.am b/src/libguac/tests/Makefile.am index dba842419..1d2f45f11 100644 --- a/src/libguac/tests/Makefile.am +++ b/src/libguac/tests/Makefile.am @@ -39,6 +39,8 @@ noinst_HEADERS = \ test_libguac_SOURCES = \ client/buffer_pool.c \ client/layer_pool.c \ + fifo/fifo.c \ + flag/flag.c \ id/generate.c \ mem/alloc.c \ mem/ckd_add.c \ @@ -56,6 +58,11 @@ test_libguac_SOURCES = \ pool/next_free.c \ protocol/base64_decode.c \ protocol/guac_protocol_version.c \ + rect/align.c \ + rect/constrain.c \ + rect/extend.c \ + rect/init.c \ + rect/intersects.c \ socket/fd_send_instruction.c \ socket/nested_send_instruction.c \ string/strdup.c \ diff --git a/src/libguac/tests/fifo/fifo.c b/src/libguac/tests/fifo/fifo.c new file mode 100644 index 000000000..67617e2f3 --- /dev/null +++ b/src/libguac/tests/fifo/fifo.c @@ -0,0 +1,298 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +/** + * The maximum number of milliseconds to wait for a test event to be added to a + * fifo. + */ +#define TEST_TIMEOUT 250 + +/** + * The maximum number of items permitted in test_fifo. + */ +#define TEST_FIFO_MAX_ITEMS 4 + +/** + * The rough amount of time to wait between fifo reads within the test thread, + * in milliseconds. A random delay between 0ms and this value will be added + * before each read. This is done to verify that the fifo behaves correctly + * for cases where the sending thread is producing data much faster than it's + * being read, slower than it's read, etc. + */ +#define TEST_READ_INTERVAL 10 + +/** + * Zero-terminated set of arbitrarily-chosen values that will be provided as + * the test_value of a sequence of test_events. + */ +unsigned int TEST_VALUES[] = { + 32, 32, 226, 136, 167, 44, 44, 44, + 226, 136, 167, 32, 32, 32, 32, 32, + 65, 112, 97, 119, 99, 104, 101, 10, + 32, 40, 226, 128, 162, 32, 226, 169, + 138, 32, 226, 128, 162, 41, 32, 32, + 71, 117, 97, 99, 97, 109, 101, 111, + 119, 108, 101, 33, 10, /* END */ 0 +}; + +/** + * Test event for an event fifo. This particular event contains a single + * integer for verifying that events are received in the order expected, and a + * chunk of arbitrary padding to ensure the base fifo is capable of supporting + * events of arbitrary size. + */ +typedef union test_event { + + /** + * Arbitrary integer test value. This value is primarily intended to allow + * unit tests to verify the order of received events matches the order they + * were sent. + */ + unsigned int test_value; + + /** + * Arbitrary padding. This member is entirely ignored and is used only to + * increase the storage size of this event. A wonky prime value is used + * here to help ensure the tests inherently verify that the base fifo + * implementation does not somehow depend on power-of-two alignment. + */ + char padding[73]; + +} test_event; + +/** + * Test event fifo that extends the guac_fifo base. This event + * fifo differs from the base only in that it specifically stores test_events + * alongside an array of expected event values. + */ +typedef struct test_fifo { + + /** + * The base fifo implementation. + */ + guac_fifo base; + + /** + * Storage for all event items in this fifo. + */ + test_event items[TEST_FIFO_MAX_ITEMS]; + + /** + * A zero-terminated array of all integer values expected to be received as + * test events, in the order that they are expected to be received. + */ + unsigned int* expected_values; + +} test_fifo; + +/** + * Initializes the given test_fifo, assigning the given set of expected + * values for later reference by unit tests. The pointer to the expected values + * MUST remain valid until the text_fifo is destroyed. + * + * @param fifo + * The test_fifo to initialize. + * + * @param expected_values + * The zero-terminated set of expected values to be associated with the + * given test_fifo. + */ +void test_fifo_init(test_fifo* fifo, unsigned int* expected_values) { + + guac_fifo_init((guac_fifo*) fifo, &fifo->items, + TEST_FIFO_MAX_ITEMS, sizeof(test_event)); + + fifo->expected_values = expected_values; + +} + +/** + * Destroys the given test_fifo, releasing any associated resources. It + * is safe to clean up the set of expected values originally provided to + * test_fifo_init() after this function has been invoked. + * + * @param fifo + * The test_fifo to destroy. + */ +void test_fifo_destroy(test_fifo* fifo) { + guac_fifo_destroy((guac_fifo*) fifo); +} + +/** + * Thread that continuously reads events from the given test_fifo, + * verifying that each expected value is read in the correct order. + * + * @param data + * The test_fifo to read from. + * + * @return + * Always NULL. + */ +static void* queue_read_thread(void* data) { + + test_fifo* fifo = (test_fifo*) data; + test_event event; + + /* Continuously read values until zero (end of expected values) is reached */ + for (unsigned int* current_expected_value = fifo->expected_values; + /* Exit condition checked in body of loop*/; current_expected_value++) { + + /* Induce random delays in reading to simulate real-world conditions + * that may cause the fifo to fill */ + guac_timestamp_msleep(rand() % TEST_READ_INTERVAL); + + int retval = guac_fifo_timed_dequeue( + (guac_fifo*) fifo, &event, TEST_TIMEOUT); + + /* A value of zero marks the end of the set of expected values, so the + * fifo SHOULD fail to read at this point */ + if (*current_expected_value == 0) { + printf(" | END\n"); + CU_ASSERT_FALSE(retval); + break; + } + + /* For all other cases, the fifo should succeed in reading the next + * event, and the value of that event should match the current value + * from the set of expected values */ + else { + printf(" | %i\n", event.test_value); + CU_ASSERT_TRUE(retval); + CU_ASSERT_EQUAL(event.test_value, *current_expected_value); + } + + /* Do not continue waiting for more events if the fifo is timing out + * incorrectly */ + if (!retval) + break; + + } + + return NULL; + +} + +/** + * Generic base test that sends all values in TEST_VALUES at the given + * interval. Values are read by a separate thread that instead reads at + * TEST_READ_INTERVAL, allowing the send/receive rates to differ. Timing + * between each send/receive attempt is varied randomly but is always bounded + * by the relevant interval. + * + * @param send_interval + * The rough number of milliseconds to wait between sending each event. The + * true number of milliseconds that elapse between each subsequent send + * attempt is varied randomly, with this provided value functioning as an + * upper bound. + */ +static void verify_send_receive(int send_interval) { + + test_fifo fifo; + + /* Create a test fifo that verifies each value within TEST_VALUES is + * received in order */ + test_fifo_init(&fifo, TEST_VALUES); + + /* Both this function and the thread it spawns will log sent/received event + * values to STDOUT for sake of debugging and verification */ + printf("Sent | Received\n" + "---- | --------\n"); + + /* Spawn thread that can independently wait for events to be flagged */ + pthread_t test_thread; + CU_ASSERT_FALSE_FATAL(pthread_create(&test_thread, NULL, queue_read_thread, &fifo)); + + /* Send all test values in order */ + for (unsigned int* current = TEST_VALUES; *current != 0; current++) { + + /* Pull next test value from TEST_VALUES array */ + test_event event = { + .test_value = *current + }; + + /* Induce random delays in reading to simulate real-world conditions + * that may cause the fifo to fill */ + if (send_interval) + guac_timestamp_msleep(rand() % send_interval); + + printf("%4i |\n", event.test_value); + guac_fifo_enqueue((guac_fifo*) &fifo, &event); + + } + + /* All test values have now been sent */ + printf(" END |\n"); + + /* Wait for thread to finish waiting for events */ + CU_ASSERT_FALSE(pthread_join(test_thread, NULL)); + + test_fifo_destroy(&fifo); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent slower than they are read. + */ +void test_fifo__slow_add() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send at half the speed of the reading thread */ + verify_send_receive(TEST_READ_INTERVAL * 2); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent faster than they are read. + */ +void test_fifo__fast_add() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send as quickly as possible (much faster than reading thread) */ + verify_send_receive(0); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent at roughly the same speed as the reading thread. + */ +void test_fifo__interleaved() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send at roughly same speed as reading thread */ + verify_send_receive(TEST_READ_INTERVAL); + +} + diff --git a/src/libguac/tests/flag/flag.c b/src/libguac/tests/flag/flag.c new file mode 100644 index 000000000..c822a6a7e --- /dev/null +++ b/src/libguac/tests/flag/flag.c @@ -0,0 +1,168 @@ +/* + * 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 +#include +#include +#include + +/** + * The maximum number of milliseconds to wait for a test event to be flagged. + */ +#define TEST_TIMEOUT 250 + +/** + * Arbitrary test event #1. + */ +#define TEST_EVENT_A 1 + +/** + * Arbitrary test event #2. + */ +#define TEST_EVENT_B 2 + +/** + * Arbitrary test event #3. + */ +#define TEST_EVENT_C 16 + +/** + * Arbitrary test event #4. + */ +#define TEST_EVENT_D 64 + +/** + * Thread that waits up to TEST_TIMEOUT milliseconds for TEST_EVENT_B or + * TEST_EVENT_C to be flagged on a given guac_flag, returning the + * result of that wait. + * + * @param data + * The guac_flag to wait on. + * + * @return + * An intptr_t (NOT a pointer) containing the value returned by + * guac_flag_timedwait_and_lock(). + */ +static void* flag_wait_thread(void* data) { + + guac_flag* flag = (guac_flag*) data; + + int retval = guac_flag_timedwait_and_lock(flag, TEST_EVENT_B | TEST_EVENT_C, TEST_TIMEOUT); + guac_flag_unlock(flag); + + return (void*) ((intptr_t) retval); + +} + +/** + * Waits up to TEST_TIMEOUT milliseconds for TEST_EVENT_B or TEST_EVENT_C to be + * flagged on the given guac_flag, returning the result of that + * wait. If provided, optional sets of flags will be additionally set or + * cleared after the wait for the flag has started. + * + * @param flag + * The guac_flag to wait on. + * + * @param set_flags + * The flags that should be set, if any. + * + * @param clear_flags + * The flags that should be cleared, if any. + * + * @return + * The value returned by guac_flag_timedwait_and_lock() after + * waiting for TEST_EVENT_B or TEST_EVENT_C to be flagged. + */ +static int wait_for_flag(guac_flag* flag, int set_flags, int clear_flags) { + + /* Spawn thread that can independently wait for events to be flagged */ + pthread_t test_thread; + CU_ASSERT_FALSE_FATAL(pthread_create(&test_thread, NULL, flag_wait_thread, flag)); + + /* Set/clear any requested event flags */ + if (set_flags) guac_flag_set(flag, set_flags); + if (clear_flags) guac_flag_clear(flag, clear_flags); + + /* Wait for thread to finish waiting for events */ + void* retval; + CU_ASSERT_FALSE(pthread_join(test_thread, &retval)); + + return (int) ((intptr_t) retval); + +} + +/** + * Verifies that a thread waiting on a particular event will NOT be notified if + * absolutely zero events ever occur. + */ +void test_flag__ignore_total_silence() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify no interesting events occur if we set zero flags */ + CU_ASSERT_FALSE(wait_for_flag(&test_flag, 0, 0)); + + guac_flag_destroy(&test_flag); + +} + +/** + * Verifies that a thread waiting on a particular event will NOT be notified if + * that event never occurs, even if other events are occurring. + */ +void test_flag__ignore_uninteresting_events() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify no interesting events occurred if we only fire uninteresting + * events */ + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_A, 0)); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_D, TEST_EVENT_C)); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_A | TEST_EVENT_D, 0)); + + guac_flag_destroy(&test_flag); + +} + +/** + * Verifies that a thread waiting on a particular event will be notified when + * that event occurs. + */ +void test_flag__wake_for_interesting_events() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify interesting events are reported if fired ... */ + CU_ASSERT_TRUE(wait_for_flag(&test_flag, TEST_EVENT_B | TEST_EVENT_C, 0)); + + /* ... and continue to be reported if they remain set ... */ + guac_flag_clear(&test_flag, TEST_EVENT_B); + CU_ASSERT_TRUE(wait_for_flag(&test_flag, 0, 0)); + + /* ... but not if all interesting events have since been cleared */ + guac_flag_clear(&test_flag, TEST_EVENT_C); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, 0, 0)); + + guac_flag_destroy(&test_flag); + +} + diff --git a/src/libguac/tests/rect/align.c b/src/libguac/tests/rect/align.c new file mode 100644 index 000000000..bed197269 --- /dev/null +++ b/src/libguac/tests/rect/align.c @@ -0,0 +1,70 @@ +/* + * 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 +#include + +/** + * Test which verifies guac_rect_align() properly shifts and resizes rectangles + * to fit an NxN grid. + */ +void test_rect__align() { + + /* A cell size of 4 is 2^4 (16) */ + const int cell_size = 4; + + guac_rect rect; + + /* Simple case where only the rectangle dimensions need adjustment */ + guac_rect_init(&rect, 0, 0, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(32, rect.right); + CU_ASSERT_EQUAL(32, rect.bottom); + + /* More complex case where the rectangle location AND dimensions both need + * adjustment */ + guac_rect_init(&rect, 75, 75, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(64, rect.left); + CU_ASSERT_EQUAL(64, rect.top); + CU_ASSERT_EQUAL(112, rect.right); + CU_ASSERT_EQUAL(112, rect.bottom); + + /* Complex case where the rectangle location AND dimensions both need + * adjustment, and the rectangle location is negative */ + guac_rect_init(&rect, -5, -5, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(-16, rect.left); + CU_ASSERT_EQUAL(-16, rect.top); + CU_ASSERT_EQUAL(32, rect.right); + CU_ASSERT_EQUAL(32, rect.bottom); + + /* Complex case where the rectangle location AND dimensions both need + * adjustment, and all rectangle coordinates are negative */ + guac_rect_init(&rect, -30, -30, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(-32, rect.left); + CU_ASSERT_EQUAL(-32, rect.top); + CU_ASSERT_EQUAL(0, rect.right); + CU_ASSERT_EQUAL(0, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/constrain.c b/src/libguac/tests/rect/constrain.c new file mode 100644 index 000000000..18ddc7a87 --- /dev/null +++ b/src/libguac/tests/rect/constrain.c @@ -0,0 +1,42 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_rect_constrain() restricts a given rectangle + * to arbitrary bounds. + */ +void test_rect__constrain() { + + guac_rect max; + guac_rect rect; + + guac_rect_init(&rect, -10, -10, 110, 110); + guac_rect_init(&max, 0, 0, 100, 100); + guac_rect_constrain(&rect, &max); + + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(100, rect.right); + CU_ASSERT_EQUAL(100, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/extend.c b/src/libguac/tests/rect/extend.c new file mode 100644 index 000000000..264ab3bfc --- /dev/null +++ b/src/libguac/tests/rect/extend.c @@ -0,0 +1,41 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_rect_extend() expands the given rectangle as + * necessary to contain at least the given bounds. + */ +void test_rect__extend() { + + guac_rect max; + guac_rect rect; + + guac_rect_init(&rect, 10, 10, 90, 90); + guac_rect_init(&max, 0, 0, 100, 100); + guac_rect_extend(&rect, &max); + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(100, rect.right); + CU_ASSERT_EQUAL(100, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/init.c b/src/libguac/tests/rect/init.c new file mode 100644 index 000000000..c91efec25 --- /dev/null +++ b/src/libguac/tests/rect/init.c @@ -0,0 +1,38 @@ +/* + * 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 +#include + +/** + * Test which verifies rectangle initialization via guac_rect_init(). + */ +void test_rect__init() { + + guac_rect max; + + guac_rect_init(&max, 0, 0, 100, 100); + + CU_ASSERT_EQUAL(0, max.left); + CU_ASSERT_EQUAL(0, max.top); + CU_ASSERT_EQUAL(100, max.right); + CU_ASSERT_EQUAL(100, max.bottom); + +} + diff --git a/src/libguac/tests/rect/intersects.c b/src/libguac/tests/rect/intersects.c new file mode 100644 index 000000000..e6b005e74 --- /dev/null +++ b/src/libguac/tests/rect/intersects.c @@ -0,0 +1,85 @@ +/* + * 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 +#include + +/** + * Test which verifies intersection testing via guac_rect_intersects(). + */ +void test_rect__intersects() { + + int res; + + guac_rect min; + guac_rect rect; + + /* NOTE: This rectangle will extend from (10, 10) inclusive to (20, 20) exclusive */ + guac_rect_init(&min, 10, 10, 10, 10); + + /* Rectangle that does not intersect by a fair margin */ + guac_rect_init(&rect, 25, 25, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that barely does not intersect (one pixel away from intersecting) */ + guac_rect_init(&rect, 20, 20, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that intersects by being entirely inside the other */ + guac_rect_init(&rect, 11, 11, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the upper-left corner */ + guac_rect_init(&rect, 8, 8, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the lower-right corner */ + guac_rect_init(&rect, 18, 18, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the uppper-left corner and shares both + * the upper and left edges */ + guac_rect_init(&rect, 10, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that barely fails to intersect the upper-left corner (one + * pixel away) */ + guac_rect_init(&rect, 5, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that barely fails to intersect the upper-right corner (one + * pixel away) */ + guac_rect_init(&rect, 20, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that intersects by entirely containing the other */ + guac_rect_init(&rect, 5, 5, 20, 20); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + +} + diff --git a/src/libguac/user.c b/src/libguac/user.c index 846d0eddd..753aca3fa 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -107,13 +107,11 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { guac_stream* allocd_stream; int stream_index; - /* Refuse to allocate beyond maximum */ - if (user->__stream_pool->active == GUAC_USER_MAX_STREAMS) + /* Allocate stream, but refuse to allocate beyond maximum */ + stream_index = guac_pool_next_int_below(user->__stream_pool, GUAC_USER_MAX_STREAMS); + if (stream_index < 0) return NULL; - /* Allocate stream */ - stream_index = guac_pool_next_int(user->__stream_pool); - /* Initialize stream with even index (odd indices are client-level) */ allocd_stream = &(user->__output_streams[stream_index]); allocd_stream->index = stream_index * 2; @@ -128,12 +126,13 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { void guac_user_free_stream(guac_user* user, guac_stream* stream) { - /* Release index to pool */ - guac_pool_free_int(user->__stream_pool, stream->index / 2); - /* Mark stream as closed */ + int freed_index = stream->index; stream->index = GUAC_USER_CLOSED_STREAM_INDEX; + /* Release index to pool */ + guac_pool_free_int(user->__stream_pool, freed_index / 2); + } guac_object* guac_user_alloc_object(guac_user* user) { @@ -141,13 +140,11 @@ guac_object* guac_user_alloc_object(guac_user* user) { guac_object* allocd_object; int object_index; - /* Refuse to allocate beyond maximum */ - if (user->__object_pool->active == GUAC_USER_MAX_OBJECTS) + /* Allocate object, but refuse to allocate beyond maximum */ + object_index = guac_pool_next_int_below(user->__object_pool, GUAC_USER_MAX_OBJECTS); + if (object_index < 0) return NULL; - /* Allocate object */ - object_index = guac_pool_next_int(user->__object_pool); - /* Initialize object */ allocd_object = &(user->__objects[object_index]); allocd_object->index = object_index; @@ -161,12 +158,13 @@ guac_object* guac_user_alloc_object(guac_user* user) { void guac_user_free_object(guac_user* user, guac_object* object) { - /* Release index to pool */ - guac_pool_free_int(user->__object_pool, object->index); - /* Mark object as undefined */ + int freed_index = object->index; object->index = GUAC_USER_UNDEFINED_OBJECT_INDEX; + /* Release index to pool */ + guac_pool_free_int(user->__object_pool, freed_index); + } int guac_user_handle_instruction(guac_user* user, const char* opcode, int argc, char** argv) { diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c index 369923de2..e972520b9 100644 --- a/src/protocols/kubernetes/user.c +++ b/src/protocols/kubernetes/user.c @@ -19,7 +19,6 @@ #include "argv.h" #include "clipboard.h" -#include "common/cursor.h" #include "input.h" #include "kubernetes.h" #include "pipe.h" diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 57204f2aa..12a2e68b4 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -40,7 +40,6 @@ nodist_libguac_client_rdp_la_SOURCES = \ libguac_client_rdp_la_SOURCES = \ argv.c \ beep.c \ - bitmap.c \ channels/audio-input/audio-buffer.c \ channels/audio-input/audio-input.c \ channels/cliprdr.c \ @@ -67,7 +66,6 @@ libguac_client_rdp_la_SOURCES = \ error.c \ fs.c \ gdi.c \ - glyph.c \ input.c \ keyboard.c \ keymap.c \ @@ -87,7 +85,6 @@ libguac_client_rdp_la_SOURCES = \ noinst_HEADERS = \ argv.h \ beep.h \ - bitmap.h \ channels/audio-input/audio-buffer.h \ channels/audio-input/audio-input.h \ channels/cliprdr.h \ @@ -114,7 +111,6 @@ noinst_HEADERS = \ error.h \ fs.h \ gdi.h \ - glyph.h \ input.h \ keyboard.h \ keymap.h \ diff --git a/src/protocols/rdp/bitmap.c b/src/protocols/rdp/bitmap.c deleted file mode 100644 index 6faa151fb..000000000 --- a/src/protocols/rdp/bitmap.c +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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 "bitmap.h" -#include "common/display.h" -#include "common/surface.h" -#include "config.h" -#include "rdp.h" - -#include -#include -#include -#include -#include - -#include -#include - -void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - /* Allocate buffer */ - guac_common_display_layer* buffer = guac_common_display_alloc_buffer( - rdp_client->display, bitmap->width, bitmap->height); - - /* Cache image data if present */ - if (bitmap->data != NULL) { - - /* Create surface from image data */ - cairo_surface_t* image = cairo_image_surface_create_for_data( - bitmap->data, CAIRO_FORMAT_RGB24, - bitmap->width, bitmap->height, 4*bitmap->width); - - /* Send surface to buffer */ - guac_common_surface_draw(buffer->surface, 0, 0, image); - - /* Free surface */ - cairo_surface_destroy(image); - - } - - /* Store buffer reference in bitmap */ - ((guac_rdp_bitmap*) bitmap)->layer = buffer; - -} - -BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap) { - - /* No corresponding surface yet - caching is deferred. */ - ((guac_rdp_bitmap*) bitmap)->layer = NULL; - - /* Start at zero usage */ - ((guac_rdp_bitmap*) bitmap)->used = 0; - - return TRUE; - -} - -BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - int width = bitmap->right - bitmap->left + 1; - int height = bitmap->bottom - bitmap->top + 1; - - /* If not cached, cache if necessary */ - if (buffer == NULL && ((guac_rdp_bitmap*) bitmap)->used >= 1) - guac_rdp_cache_bitmap(context, bitmap); - - /* If cached, retrieve from cache */ - if (buffer != NULL) - guac_common_surface_copy(buffer->surface, 0, 0, width, height, - rdp_client->display->default_surface, - bitmap->left, bitmap->top); - - /* Otherwise, draw with stored image data */ - else if (bitmap->data != NULL) { - - /* Create surface from image data */ - cairo_surface_t* image = cairo_image_surface_create_for_data( - bitmap->data, CAIRO_FORMAT_RGB24, - width, height, 4*bitmap->width); - - /* Draw image on default surface */ - guac_common_surface_draw(rdp_client->display->default_surface, - bitmap->left, bitmap->top, image); - - /* Free surface */ - cairo_surface_destroy(image); - - } - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - return TRUE; - -} - -void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - /* If cached, free buffer */ - if (buffer != NULL) - guac_common_display_free_buffer(rdp_client->display, buffer); - -#ifndef FREERDP_BITMAP_FREE_FREES_BITMAP - /* NOTE: Except in FreeRDP 2.0.0-rc0 and earlier, FreeRDP-allocated memory - * for the rdpBitmap will NOT be automatically released after this free - * handler is invoked, thus we must do so manually here */ - GUAC_ALIGNED_FREE(bitmap->data); - free(bitmap); -#endif - -} - -BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - if (primary) - rdp_client->current_surface = rdp_client->display->default_surface; - - else { - - /* Make sure that the received bitmap is not NULL before processing */ - if (bitmap == NULL) { - guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in bitmap_setsurface instruction."); - return TRUE; - } - - /* If not available as a surface, make available. */ - if (((guac_rdp_bitmap*) bitmap)->layer == NULL) - guac_rdp_cache_bitmap(context, bitmap); - - rdp_client->current_surface = - ((guac_rdp_bitmap*) bitmap)->layer->surface; - - } - - return TRUE; - -} diff --git a/src/protocols/rdp/bitmap.h b/src/protocols/rdp/bitmap.h deleted file mode 100644 index 297230c50..000000000 --- a/src/protocols/rdp/bitmap.h +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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. - */ - -#ifndef GUAC_RDP_BITMAP_H -#define GUAC_RDP_BITMAP_H - -#include "config.h" -#include "common/display.h" - -#include -#include -#include -#include - -/** - * Guacamole-specific rdpBitmap data. - */ -typedef struct guac_rdp_bitmap { - - /** - * FreeRDP bitmap data - MUST GO FIRST. - */ - rdpBitmap bitmap; - - /** - * Layer containing cached image data. - */ - guac_common_display_layer* layer; - - /** - * The number of times a bitmap has been used. - */ - int used; - -} guac_rdp_bitmap; - -/** - * Caches the given bitmap immediately, storing its data in a remote Guacamole - * buffer. As RDP bitmaps are frequently created, used once, and immediately - * destroyed, we defer actual remote-side caching of RDP bitmaps until they are - * used at least once. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to cache. - */ -void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap); - -/** - * Initializes the given newly-created rdpBitmap. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to initialize. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap); - -/** - * Paints the given rdpBitmap on the primary display surface. Note that this - * operation does NOT draw to the "current" surface set by calls to - * guac_rdp_bitmap_setsurface(). - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to paint. This structure will also contain the specifics of - * the paint operation to perform, including the destination X/Y - * coordinates. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap); - -/** - * Frees any Guacamole-specific data associated with the given rdpBitmap. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap whose Guacamole-specific data is to be freed. - */ -void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap); - -/** - * Sets the given rdpBitmap as the drawing surface for future operations or, - * if the primary flag is set, resets the current drawing surface to the - * primary drawing surface of the remote display. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The rdpBitmap to set as the current drawing surface. This parameter is - * only valid if the primary flag is FALSE. - * - * @param primary - * TRUE if the bitmap parameter should be ignored, and the current drawing - * surface should be reset to the primary drawing surface of the remote - * display, FALSE otherwise. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, - BOOL primary); - -#endif diff --git a/src/protocols/rdp/channels/disp.c b/src/protocols/rdp/channels/disp.c index 81c2d3bd3..da1ca800d 100644 --- a/src/protocols/rdp/channels/disp.c +++ b/src/protocols/rdp/channels/disp.c @@ -18,7 +18,6 @@ */ #include "channels/disp.h" -#include "common/display.h" #include "plugins/channels.h" #include "fs.h" #include "rdp.h" @@ -29,6 +28,7 @@ #include #include #include +#include #include #include @@ -153,11 +153,25 @@ void guac_rdp_disp_load_plugin(rdpContext* context) { void guac_rdp_disp_set_size(guac_rdp_disp* disp, guac_rdp_settings* settings, freerdp* rdp_inst, int width, int height) { - /* Fit width within bounds, adjusting height to maintain aspect ratio */ - guac_common_display_fit(&width, &height); + guac_rect resize = { + .left = 0, + .top = 0, + .right = width, + .bottom = height + }; - /* Fit height within bounds, adjusting width to maintain aspect ratio */ - guac_common_display_fit(&height, &width); + /* Fit width and height within bounds, maintaining aspect ratio */ + guac_rect_shrink(&resize, GUAC_RDP_DISP_MAX_SIZE, GUAC_RDP_DISP_MAX_SIZE); + + width = guac_rect_width(&resize); + height = guac_rect_height(&resize); + + /* As it's possible for a rectangle to exceed the maximum allowed + * dimensions, yet fall below the minimum allowed dimensions once adjusted, + * we don't bother preserving aspect ratio for the unlikely case that a + * dimension is below the minimums (consider a rectangle like 16384x256) */ + if (width < GUAC_RDP_DISP_MIN_SIZE) width = GUAC_RDP_DISP_MIN_SIZE; + if (height < GUAC_RDP_DISP_MIN_SIZE) height = GUAC_RDP_DISP_MIN_SIZE; /* Width must be even */ if (width % 2 == 1) @@ -185,7 +199,7 @@ void guac_rdp_disp_update_size(guac_rdp_disp* disp, guac_timestamp now = guac_timestamp_current(); /* Limit display update frequency */ - if (now - disp->last_request <= GUAC_COMMON_DISPLAY_UPDATE_INTERVAL) + if (now - disp->last_request <= GUAC_RDP_DISP_UPDATE_INTERVAL) return; /* Do NOT send requests unless the size will change */ diff --git a/src/protocols/rdp/channels/disp.h b/src/protocols/rdp/channels/disp.h index 91beebad9..54fe4ccfa 100644 --- a/src/protocols/rdp/channels/disp.h +++ b/src/protocols/rdp/channels/disp.h @@ -27,6 +27,22 @@ #include #include +/** + * The minimum value for width or height, in pixels. + */ +#define GUAC_RDP_DISP_MIN_SIZE 200 + +/** + * The maximum value for width or height, in pixels. + */ +#define GUAC_RDP_DISP_MAX_SIZE 8192 + +/** + * The minimum amount of time that must elapse between display size updates, + * in milliseconds. + */ +#define GUAC_RDP_DISP_UPDATE_INTERVAL 500 + /** * Display size update module. */ diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c index 8a89af5ea..a94faa9f4 100644 --- a/src/protocols/rdp/channels/rdpei.c +++ b/src/protocols/rdp/channels/rdpei.c @@ -18,7 +18,6 @@ */ #include "channels/rdpei.h" -#include "common/surface.h" #include "plugins/channels.h" #include "rdp.h" #include "settings.h" @@ -27,6 +26,7 @@ #include #include #include +#include #include #include @@ -87,7 +87,7 @@ static void guac_rdp_rdpei_channel_connected(rdpContext* context, guac_rdpei->rdpei = rdpei; /* Declare level of multi-touch support */ - guac_common_surface_set_multitouch(rdp_client->display->default_surface, + guac_display_layer_set_multitouch(guac_display_default_layer(rdp_client->display), GUAC_RDP_RDPEI_MAX_TOUCHES); guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for " diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 4d9ba8f6c..263ab8d20 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -132,7 +132,7 @@ static int guac_rdp_join_pending_handler(guac_client* client) { /* Synchronize with current display */ if (rdp_client->display != NULL) { - guac_common_display_dup(rdp_client->display, client, broadcast_socket); + guac_display_dup(rdp_client->display, broadcast_socket); guac_socket_flush(broadcast_socket); } diff --git a/src/protocols/rdp/client.h b/src/protocols/rdp/client.h index 943d39abb..7955db364 100644 --- a/src/protocols/rdp/client.h +++ b/src/protocols/rdp/client.h @@ -23,25 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The amount of time to wait for new messages from the RDP server before + * moving on to internal matters, in milliseconds. This value must be kept + * reasonably small such that a slow RDP server will not prevent external + * events from being handled (such as the stop signal from guac_client_stop()), + * but large enough that the message handling loop does not eat up CPU + * spinning. */ -#define GUAC_RDP_FRAME_DURATION 60 - -/** - * The amount of time to allow per message read within a frame, in - * milliseconds. If the server is silent for at least this amount of time, the - * frame will be considered finished. - */ -#define GUAC_RDP_FRAME_TIMEOUT 0 - -/** - * The amount of time to wait for a new message from the RDP server when - * beginning a new frame, in milliseconds. This value must be kept reasonably - * small such that a slow RDP server will not prevent external events from - * being handled (such as the stop signal from guac_client_stop()), but large - * enough that the message handling loop does not eat up CPU spinning. - */ -#define GUAC_RDP_FRAME_START_TIMEOUT 250 +#define GUAC_RDP_MESSAGE_CHECK_INTERVAL 1000 /** * The native resolution of most RDP connections. As Windows and other systems diff --git a/src/protocols/rdp/fs.c b/src/protocols/rdp/fs.c index be228728c..80bdacdcd 100644 --- a/src/protocols/rdp/fs.c +++ b/src/protocols/rdp/fs.c @@ -361,7 +361,7 @@ int guac_rdp_fs_open(guac_rdp_fs* fs, const char* path, } /* Get file ID, init file */ - file_id = guac_pool_next_int(fs->file_id_pool); + file_id = guac_pool_next_int_below_or_die(fs->file_id_pool, GUAC_RDP_FS_MAX_FILES); file = &(fs->files[file_id]); file->id = file_id; file->fd = fd; diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index 61bd87dd6..69373faf9 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -17,10 +17,7 @@ * under the License. */ -#include "bitmap.h" #include "color.h" -#include "common/display.h" -#include "common/surface.h" #include "rdp.h" #include "settings.h" @@ -29,377 +26,22 @@ #include #include #include +#include #include +#include #include #include #include -guac_transfer_function guac_rdp_rop3_transfer_function(guac_client* client, - int rop3) { - - /* Translate supported ROP3 opcodes into composite modes */ - switch (rop3) { - - /* "DSon" !(src | dest) */ - case 0x11: return GUAC_TRANSFER_BINARY_NOR; - - /* "DSna" !src & dest */ - case 0x22: return GUAC_TRANSFER_BINARY_NSRC_AND; - - /* "Sn" !src */ - case 0x33: return GUAC_TRANSFER_BINARY_NSRC; - - /* "SDna" (src & !dest) */ - case 0x44: return GUAC_TRANSFER_BINARY_NDEST_AND; - - /* "Dn" !dest */ - case 0x55: return GUAC_TRANSFER_BINARY_NDEST; - - /* "SRCINVERT" (src ^ dest) */ - case 0x66: return GUAC_TRANSFER_BINARY_XOR; - - /* "DSan" !(src & dest) */ - case 0x77: return GUAC_TRANSFER_BINARY_NAND; - - /* "SRCAND" (src & dest) */ - case 0x88: return GUAC_TRANSFER_BINARY_AND; - - /* "DSxn" !(src ^ dest) */ - case 0x99: return GUAC_TRANSFER_BINARY_XNOR; - - /* "MERGEPAINT" (!src | dest)*/ - case 0xBB: return GUAC_TRANSFER_BINARY_NSRC_OR; - - /* "SDno" (src | !dest) */ - case 0xDD: return GUAC_TRANSFER_BINARY_NDEST_OR; - - /* "SRCPAINT" (src | dest) */ - case 0xEE: return GUAC_TRANSFER_BINARY_OR; - - /* 0x00 = "BLACKNESS" (0) */ - /* 0xAA = "NOP" (dest) */ - /* 0xCC = "SRCCOPY" (src) */ - /* 0xFF = "WHITENESS" (1) */ - - } - - /* Log warning if ROP3 opcode not supported */ - guac_client_log(client, GUAC_LOG_INFO, "guac_rdp_rop3_transfer_function: " - "UNSUPPORTED opcode = 0x%02X", rop3); - - /* Default to BINARY_SRC */ - return GUAC_TRANSFER_BINARY_SRC; - -} - -BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = dstblt->nLeftRect; - int y = dstblt->nTopRect; - int w = dstblt->nWidth; - int h = dstblt->nHeight; - - switch (dstblt->bRop) { - - /* Blackness */ - case 0: - - /* Send black rectangle */ - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; - - /* DSTINVERT */ - case 0x55: - guac_common_surface_transfer(current_surface, x, y, w, h, - GUAC_TRANSFER_BINARY_NDEST, current_surface, x, y); - break; - - /* NOP */ - case 0xAA: - break; - - /* Whiteness */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; - - /* Unsupported ROP3 */ - default: - guac_client_log(client, GUAC_LOG_INFO, - "guac_rdp_gdi_dstblt(rop3=0x%x)", dstblt->bRop); - - } - - return TRUE; - -} - -BOOL guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt) { - - /* - * Note that this is not a full implementation of PATBLT. This is a - * fallback implementation which only renders a solid block of background - * color using the specified ROP3 operation, ignoring whatever brush - * was actually specified. - * - * As libguac-client-rdp explicitly tells the server not to send PATBLT, - * well-behaved RDP servers will not use this operation at all, while - * others will at least have a fallback. - */ - - /* Get client and current layer */ - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = - ((guac_rdp_client*) client->data)->current_surface; - - int x = patblt->nLeftRect; - int y = patblt->nTopRect; - int w = patblt->nWidth; - int h = patblt->nHeight; - - /* - * Warn that rendering is a fallback, as the server should not be sending - * this order. - */ - guac_client_log(client, GUAC_LOG_INFO, "Using fallback PATBLT (server is ignoring " - "negotiated client capabilities)"); - - /* Render rectangle based on ROP */ - switch (patblt->bRop) { - - /* If blackness, send black rectangle */ - case 0x00: - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; - - /* If NOP, do nothing */ - case 0xAA: - break; - - /* If operation is just a copy, send foreground only */ - case 0xCC: - case 0xF0: - guac_common_surface_set(current_surface, x, y, w, h, - (patblt->foreColor >> 16) & 0xFF, - (patblt->foreColor >> 8 ) & 0xFF, - (patblt->foreColor ) & 0xFF, - 0xFF); - break; - - /* If whiteness, send white rectangle */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; - - /* Otherwise, invert entire rect */ - default: - guac_common_surface_transfer(current_surface, x, y, w, h, - GUAC_TRANSFER_BINARY_NDEST, current_surface, x, y); - - } - - return TRUE; - -} - -BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = scrblt->nLeftRect; - int y = scrblt->nTopRect; - int w = scrblt->nWidth; - int h = scrblt->nHeight; - - int x_src = scrblt->nXSrc; - int y_src = scrblt->nYSrc; - - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - /* Copy screen rect to current surface */ - guac_common_surface_copy(rdp_client->display->default_surface, - x_src, y_src, w, h, current_surface, x, y); - - return TRUE; - -} - -BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - guac_rdp_bitmap* bitmap = (guac_rdp_bitmap*) memblt->bitmap; - - int x = memblt->nLeftRect; - int y = memblt->nTopRect; - int w = memblt->nWidth; - int h = memblt->nHeight; - - int x_src = memblt->nXSrc; - int y_src = memblt->nYSrc; - - /* Make sure that the received bitmap is not NULL before processing */ - if (bitmap == NULL) { - guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in memblt instruction."); - return TRUE; - } - - switch (memblt->bRop) { - - /* If blackness, send black rectangle */ - case 0x00: - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; - - /* If NOP, do nothing */ - case 0xAA: - break; - - /* If operation is just SRC, simply copy */ - case 0xCC: - - /* If not cached, cache if necessary */ - if (bitmap->layer == NULL && bitmap->used >= 1) - guac_rdp_cache_bitmap(context, memblt->bitmap); - - /* If not cached, send as PNG */ - if (bitmap->layer == NULL) { - if (memblt->bitmap->data != NULL) { - - /* Create surface from image data */ - cairo_surface_t* surface = cairo_image_surface_create_for_data( - memblt->bitmap->data + 4*(x_src + y_src*memblt->bitmap->width), - CAIRO_FORMAT_RGB24, w, h, 4*memblt->bitmap->width); - - /* Send surface to buffer */ - guac_common_surface_draw(current_surface, x, y, surface); - - /* Free surface */ - cairo_surface_destroy(surface); - - } - } - - /* Otherwise, copy */ - else - guac_common_surface_copy(bitmap->layer->surface, - x_src, y_src, w, h, current_surface, x, y); - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - break; - - /* If whiteness, send white rectangle */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; - - /* Otherwise, use transfer */ - default: - - /* If not available as a surface, make available. */ - if (bitmap->layer == NULL) - guac_rdp_cache_bitmap(context, memblt->bitmap); - - guac_common_surface_transfer(bitmap->layer->surface, - x_src, y_src, w, h, - guac_rdp_rop3_transfer_function(client, memblt->bRop), - current_surface, x, y); - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - } - - return TRUE; - -} - -BOOL guac_rdp_gdi_opaquerect(rdpContext* context, const OPAQUE_RECT_ORDER* opaque_rect) { - - /* Get client data */ - guac_client* client = ((rdp_freerdp_context*) context)->client; - - UINT32 color = guac_rdp_convert_color(context, opaque_rect->color); - - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = opaque_rect->nLeftRect; - int y = opaque_rect->nTopRect; - int w = opaque_rect->nWidth; - int h = opaque_rect->nHeight; - - guac_common_surface_set(current_surface, x, y, w, h, - (color >> 16) & 0xFF, - (color >> 8 ) & 0xFF, - (color ) & 0xFF, - 0xFF); - - return TRUE; - -} - -BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - /* If no bounds given, clear bounding rect */ - if (bounds == NULL) - guac_common_surface_reset_clip(rdp_client->display->default_surface); - - /* Otherwise, set bounding rectangle */ - else - guac_common_surface_clip(rdp_client->display->default_surface, - bounds->left, bounds->top, - bounds->right - bounds->left + 1, - bounds->bottom - bounds->top + 1); - - return TRUE; - -} - void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - /* The server supports defining explicit frames */ - rdp_client->frames_supported = 1; - - /* A new frame is beginning */ - if (starting) { - rdp_client->in_frame = 1; - return; - } - - /* The current frame has ended */ - guac_timestamp frame_end = guac_timestamp_current(); - int time_elapsed = frame_end - client->last_sent_timestamp; - rdp_client->in_frame = 0; - /* A new frame has been received from the RDP server and processed */ - rdp_client->frames_received++; - - /* Flush a new frame if the client is ready for it */ - if (time_elapsed >= guac_client_get_processing_lag(client)) { - guac_common_display_flush(rdp_client->display); - guac_client_end_multiple_frames(client, rdp_client->frames_received); - guac_socket_flush(client->socket); - rdp_client->frames_received = 0; - } + if (!starting) + guac_display_render_thread_notify_frame(rdp_client->render_thread); } @@ -431,10 +73,19 @@ BOOL guac_rdp_gdi_begin_paint(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + rdpGdi* gdi = context->gdi; + + GUAC_ASSERT(rdp_client->current_context == NULL); - /* Leverage BeginPaint handler to detect start of frame for RDPGFX channel */ - if (rdp_client->settings->enable_gfx && rdp_client->frames_supported) - guac_rdp_gdi_mark_frame(context, 1); + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); + rdp_client->current_context = current_context; + + /* Resynchronize default layer buffer details with FreeRDP's GDI */ + current_context->buffer = gdi->primary_buffer; + current_context->stride = gdi->stride; + guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); return TRUE; @@ -446,39 +97,43 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; rdpGdi* gdi = context->gdi; - /* Ignore EndPaint handler unless needed to detect end of frame for RDPGFX - * channel */ - if (!rdp_client->settings->enable_gfx) - return TRUE; + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = rdp_client->current_context; + GUAC_ASSERT(current_context != NULL); /* Ignore paint if GDI output is suppressed */ if (gdi->suppressOutput) - return TRUE; + goto paint_complete; /* Ignore paint if nothing has been done (empty rect) */ if (gdi->primary->hdc->hwnd->invalid->null) - return TRUE; + goto paint_complete; INT32 x = gdi->primary->hdc->hwnd->invalid->x; INT32 y = gdi->primary->hdc->hwnd->invalid->y; UINT32 w = gdi->primary->hdc->hwnd->invalid->w; UINT32 h = gdi->primary->hdc->hwnd->invalid->h; - /* Create surface from image data */ - cairo_surface_t* surface = cairo_image_surface_create_for_data( - gdi->primary_buffer + 4*x + y*gdi->stride, - CAIRO_FORMAT_RGB24, w, h, gdi->stride); + /* guac_rect uses signed arithmetic for all values. While FreeRDP + * definitely performs its own checks and ensures these values cannot get + * so large as to cause problems with signed arithmetic, it's worth + * checking and bailing out here if an external bug breaks that. */ + GUAC_ASSERT(w <= INT_MAX && h <= INT_MAX); + + /* Mark modified region as dirty, but only within the bounds of the + * rendering surface */ + guac_rect dst_rect; + guac_rect_init(&dst_rect, x, y, w, h); + guac_rect_constrain(&dst_rect, ¤t_context->bounds); + guac_rect_extend(¤t_context->dirty, &dst_rect); - /* Send surface to buffer */ - guac_common_surface_draw(rdp_client->display->default_surface, x, y, surface); + guac_display_render_thread_notify_modified(rdp_client->render_thread); - /* Free surface */ - cairo_surface_destroy(surface); +paint_complete: - /* Next frame */ - if (gdi->inGfxFrame) { - guac_rdp_gdi_mark_frame(context, 0); - } + /* There will be no further drawing operations */ + rdp_client->current_context = NULL; + guac_display_layer_close_raw(default_layer, current_context); return TRUE; @@ -488,18 +143,34 @@ BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + rdpGdi* gdi = context->gdi; + + int width = guac_rdp_get_width(context->instance); + int height = guac_rdp_get_height(context->instance); - guac_common_surface_resize(rdp_client->display->default_surface, - guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); + GUAC_ASSERT(rdp_client->current_context == NULL); - guac_common_surface_reset_clip(rdp_client->display->default_surface); + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); + /* Resize FreeRDP's GDI buffer */ + BOOL retval = gdi_resize(context->gdi, width, height); + GUAC_ASSERT(gdi->primary_buffer != NULL); + + /* Update our reference to the GDI buffer, as well as any structural + * details, which may now all be different */ + current_context->buffer = gdi->primary_buffer; + current_context->stride = gdi->stride; + guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); + + /* Resize layer to match new display dimensions and underlying buffer */ + guac_display_layer_resize(default_layer, gdi->width, gdi->height); guac_client_log(client, GUAC_LOG_DEBUG, "Server resized display to %ix%i", - guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); + gdi->width, gdi->height); + + guac_display_layer_close_raw(default_layer, current_context); - return gdi_resize(context->gdi, guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); + return retval; } diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h index 955bf7bd5..c6cfeb62a 100644 --- a/src/protocols/rdp/gdi.h +++ b/src/protocols/rdp/gdi.h @@ -25,136 +25,6 @@ #include #include -/** - * Translates a standard RDP ROP3 value into a guac_composite_mode. Valid - * ROP3 operations indexes are listed in the RDP protocol specifications: - * - * http://msdn.microsoft.com/en-us/library/cc241583.aspx - * - * @param client - * The guac_client associated with the current RDP session. - * - * @param rop3 - * The ROP3 operation index to translate. - * - * @return - * The guac_composite_mode that equates to, or most closely approximates, - * the given ROP3 operation. - */ -guac_composite_mode guac_rdp_rop3_transfer_function(guac_client* client, - int rop3); - -/** - * Handler for the DstBlt Primary Drawing Order. A DstBlt Primary Drawing Order - * paints a rectangle of image data using a raster operation which considers - * the destination only. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/87ea30df-59d6-438e-a735-83f0225fbf91 - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param dstblt - * The DSTBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt); - -/** - * Handler for the PatBlt Primary Drawing Order. A PatBlt Primary Drawing Order - * paints a rectangle of image data, a brush pattern, and a three-way raster - * operation which considers the source data, the destination, AND the brush - * pattern. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/bd4bf5e7-b988-45f9-8201-3b22cc9aeeb8 - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param patblt - * The PATBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt); - -/** - * Handler for the ScrBlt Primary Drawing Order. A ScrBlt Primary Drawing Order - * paints a rectangle of image data using a raster operation which considers - * the source and destination. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/a4e322b0-cd64-4dfc-8e1a-f24dc0edc99d - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param scrblt - * The SCRBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt); - -/** - * Handler for the MemBlt Primary Drawing Order. A MemBlt Primary Drawing Order - * paints a rectangle of cached image data from a cached surface to the screen - * using a raster operation which considers the source and destination. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/84c2ec2f-f776-405b-9b48-6894a28b1b14 - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param memblt - * The MEMBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt); - -/** - * Handler for the OpaqueRect Primary Drawing Order. An OpaqueRect Primary - * Drawing Order draws an opaque rectangle of a single solid color. Note that - * support for OpaqueRect cannot be claimed without also supporting PatBlt, as - * both use the same negotiation order number. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/1eead7aa-ac63-411a-9f8c-b1b227526877 - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param opaque_rect - * The OPAQUE RECT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_opaquerect(rdpContext* context, - const OPAQUE_RECT_ORDER* opaque_rect); - -/** - * Handler called prior to calling the handlers for specific updates when - * those updates are clipped by a bounding rectangle. This is not a true RDP - * update, but is called by FreeRDP before and after any update involving - * clipping. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bounds - * The clipping rectangle to set, or NULL to remove any applied clipping - * rectangle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds); - /** * Notifies the internal GDI implementation that a frame is either starting or * ending. If the frame is ending and the connected client is ready to receive @@ -215,9 +85,8 @@ BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_ BOOL guac_rdp_gdi_begin_paint(rdpContext* context); /** - * Handler called when a paint operation is complete. This function is - * expected to be called by the FreeRDP GDI implementation of RemoteFX when a - * new frame has been completed. + * Handler called when FreeRDP has finished performing updates to the backing + * surface of its GDI (graphics) implementation. * * @param context * The rdpContext associated with the current RDP session. diff --git a/src/protocols/rdp/glyph.c b/src/protocols/rdp/glyph.c deleted file mode 100644 index e0907956b..000000000 --- a/src/protocols/rdp/glyph.c +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 "color.h" -#include "common/surface.h" -#include "config.h" -#include "glyph.h" -#include "rdp.h" - -#include -#include -#include -#include - -#include -#include - -/* Define cairo_format_stride_for_width() if missing */ -#ifndef HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH -#define cairo_format_stride_for_width(format, width) (width*4) -#endif - -BOOL guac_rdp_glyph_new(rdpContext* context, GLYPH_NEW_CONST rdpGlyph* glyph) { - - int x, y, i; - int stride; - unsigned char* image_buffer; - unsigned char* image_buffer_row; - - unsigned char* data = glyph->aj; - int width = glyph->cx; - int height = glyph->cy; - - /* Init Cairo buffer */ - stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); - image_buffer = guac_mem_alloc(height, stride); - image_buffer_row = image_buffer; - - /* Copy image data from image data to buffer */ - for (y = 0; ysurface = cairo_image_surface_create_for_data( - image_buffer, CAIRO_FORMAT_ARGB32, width, height, stride); - - return TRUE; - -} - -BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h, - GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy, - BOOL redundant) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_surface* current_surface = rdp_client->current_surface; - uint32_t fgcolor = rdp_client->glyph_color; - - /* Paint with glyph as mask */ - guac_common_surface_paint(current_surface, x, y, ((guac_rdp_glyph*) glyph)->surface, - (fgcolor & 0xFF0000) >> 16, - (fgcolor & 0x00FF00) >> 8, - fgcolor & 0x0000FF); - - return TRUE; - -} - -void guac_rdp_glyph_free(rdpContext* context, rdpGlyph* glyph) { - - unsigned char* image_buffer = cairo_image_surface_get_data( - ((guac_rdp_glyph*) glyph)->surface); - - /* Free surface */ - cairo_surface_destroy(((guac_rdp_glyph*) glyph)->surface); - guac_mem_free(image_buffer); - - /* NOTE: FreeRDP-allocated memory for the rdpGlyph will NOT be - * automatically released after this free handler is invoked, thus we must - * do so manually here */ - - free(glyph->aj); - free(glyph); - -} - -BOOL guac_rdp_glyph_begindraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor, BOOL redundant) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = - (guac_rdp_client*) client->data; - - /* Fill background with color if specified */ - if (width != 0 && height != 0 && !redundant) { - - /* Convert background color */ - bgcolor = guac_rdp_convert_color(context, bgcolor); - - guac_common_surface_set(rdp_client->current_surface, - x, y, width, height, - (bgcolor & 0xFF0000) >> 16, - (bgcolor & 0x00FF00) >> 8, - (bgcolor & 0x0000FF), - 0xFF); - - } - - /* Convert foreground color */ - rdp_client->glyph_color = guac_rdp_convert_color(context, fgcolor); - - return TRUE; - -} - -BOOL guac_rdp_glyph_enddraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor) { - /* IGNORE */ - return TRUE; -} diff --git a/src/protocols/rdp/glyph.h b/src/protocols/rdp/glyph.h deleted file mode 100644 index 36b8f3a3a..000000000 --- a/src/protocols/rdp/glyph.h +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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. - */ - -#ifndef GUAC_RDP_GLYPH_H -#define GUAC_RDP_GLYPH_H - -#include "config.h" - -#include -#include -#include -#include - -#ifdef FREERDP_GLYPH_CALLBACKS_ACCEPT_INT32 -/** - * FreeRDP 2.0.0-rc4 and newer requires INT32 for all integer arguments of - * glyph callbacks. - */ -#define GLYPH_CALLBACK_INT32 INT32 -#else -/** - * FreeRDP 2.0.0-rc3 and older requires UINT32 for all integer arguments of - * glyph callbacks. - */ -#define GLYPH_CALLBACK_INT32 UINT32 -#endif - -#ifdef RDP_GLYPH_NEW_REQUIRES_CONST -#define GLYPH_NEW_CONST const -#else -#define GLYPH_NEW_CONST -#endif - -/** - * Guacamole-specific rdpGlyph data. - */ -typedef struct guac_rdp_glyph { - - /** - * FreeRDP glyph data - MUST GO FIRST. - */ - rdpGlyph glyph; - - /** - * Cairo surface layer containing cached image data. - */ - cairo_surface_t* surface; - -} guac_rdp_glyph; - -/** - * Caches the given glyph. Note that this caching currently only occurs server- - * side, as it is more efficient to transmit the text as PNG. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The glyph to cache. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_new(rdpContext* context, GLYPH_NEW_CONST rdpGlyph* glyph); - -/** - * Draws a previously-cached glyph at the given coordinates within the current - * drawing surface. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The cached glyph to draw. - * - * @param x - * The destination X coordinate of the upper-left corner of the glyph. - * - * @param y - * The destination Y coordinate of the upper-left corner of the glyph. - * - * @param w - * The width of the glyph being drawn. - * - * @param h - * The height of the glyph being drawn. - * - * @param sx - * The X coordinate of the upper-left corner of the glyph within the source - * cache surface containing the glyph. - * - * @param sy - * The Y coordinate of the upper-left corner of the glyph within the source - * cache surface containing the glyph. - * - * @param redundant - * Whether the background rectangle specified is redundant (transparent). - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h, - GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy, - BOOL redundant); - -/** - * Frees any Guacamole-specific data associated with the given glyph, such that - * it can be safely freed by FreeRDP. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The cached glyph to free. - */ -void guac_rdp_glyph_free(rdpContext* context, rdpGlyph* glyph); - -/** - * Called just prior to rendering a series of glyphs. After this function is - * called, the glyphs will be individually rendered by calls to - * guac_rdp_glyph_draw(). - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param x - * The X coordinate of the upper-left corner of the background rectangle of - * the drawing operation, or 0 if the background is transparent. - * - * @param y - * The Y coordinate of the upper-left corner of the background rectangle of - * the drawing operation, or 0 if the background is transparent. - * - * @param width - * The width of the background rectangle of the drawing operation, or 0 if - * the background is transparent. - * - * @param height - * The height of the background rectangle of the drawing operation, or 0 if - * the background is transparent. - * - * @param fgcolor - * The foreground color of each glyph. This color will be in the colorspace - * of the RDP session, and may even be a palette index, and must be - * translated via guac_rdp_convert_color(). - * - * @param bgcolor - * The background color of the drawing area. This color will be in the - * colorspace of the RDP session, and may even be a palette index, and must - * be translated via guac_rdp_convert_color(). If the background is - * transparent, this value is undefined. - * - * @param redundant - * Whether the background rectangle specified is redundant (transparent). - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_begindraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor, BOOL redundant); - -/** - * Called immediately after rendering a series of glyphs. Unlike - * guac_rdp_glyph_begindraw(), there is no way to detect through any invocation - * of this function whether the background color is opaque or transparent. We - * currently do NOT implement this function. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param x - * The X coordinate of the upper-left corner of the background rectangle of - * the drawing operation. - * - * @param y - * The Y coordinate of the upper-left corner of the background rectangle of - * the drawing operation. - * - * @param width - * The width of the background rectangle of the drawing operation. - * - * @param height - * The height of the background rectangle of the drawing operation. - * - * @param fgcolor - * The foreground color of each glyph. This color will be in the colorspace - * of the RDP session, and may even be a palette index, and must be - * translated via guac_rdp_convert_color(). - * - * @param bgcolor - * The background color of the drawing area. This color will be in the - * colorspace of the RDP session, and may even be a palette index, and must - * be translated via guac_rdp_convert_color(). If the background is - * transparent, this value is undefined. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_enddraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor); - -#endif diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index 8a8d0c6ea..d8890571e 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -19,9 +19,8 @@ #include "channels/disp.h" #include "channels/rdpei.h" -#include "common/cursor.h" -#include "common/display.h" #include "input.h" +#include "guacamole/display.h" #include "keyboard.h" #include "rdp.h" #include "settings.h" @@ -29,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +48,7 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { goto complete; /* Store current mouse location/state */ - guac_common_cursor_update(rdp_client->display->cursor, user, x, y, mask); + guac_display_notify_user_moved_mouse(rdp_client->display, user, x, y, mask); /* Report mouse position within recording */ if (rdp_client->recording != NULL) diff --git a/src/protocols/rdp/pointer.c b/src/protocols/rdp/pointer.c index 861e90670..7387b46b8 100644 --- a/src/protocols/rdp/pointer.c +++ b/src/protocols/rdp/pointer.c @@ -18,9 +18,6 @@ */ #include "color.h" -#include "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" #include "gdi.h" #include "pointer.h" #include "rdp.h" @@ -30,6 +27,7 @@ #include #include #include +#include #include #include @@ -39,34 +37,31 @@ BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; /* Allocate buffer */ - guac_common_display_layer* buffer = guac_common_display_alloc_buffer( - rdp_client->display, pointer->width, pointer->height); + guac_display_layer* buffer = guac_display_alloc_buffer(rdp_client->display, 0); - /* Allocate data for image */ - unsigned char* data = GUAC_ALIGNED_MALLOC(guac_mem_ckd_mul_or_die(pointer->width, - pointer->height, 4), 16); + guac_display_layer_resize(buffer, pointer->width, pointer->height); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(buffer); - cairo_surface_t* surface; + guac_rect dst_rect = { + .left = 0, + .top = 0, + .right = pointer->width, + .bottom = pointer->height + }; + + guac_rect_constrain(&dst_rect, &dst_context->bounds); /* Convert to alpha cursor using mask data */ - freerdp_image_copy_from_pointer_data(data, - guac_rdp_get_native_pixel_format(TRUE), 0, 0, 0, + freerdp_image_copy_from_pointer_data(GUAC_DISPLAY_LAYER_RAW_BUFFER(dst_context, dst_rect), + guac_rdp_get_native_pixel_format(TRUE), dst_context->stride, 0, 0, pointer->width, pointer->height, pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette); - /* Create surface from image data */ - surface = cairo_image_surface_create_for_data( - data, CAIRO_FORMAT_ARGB32, - pointer->width, pointer->height, 4*pointer->width); - - /* Send surface to buffer */ - guac_common_surface_draw(buffer->surface, 0, 0, surface); + guac_rect_extend(&dst_context->dirty, &dst_rect); - /* Free surface */ - cairo_surface_destroy(surface); - GUAC_ALIGNED_FREE(data); + guac_display_layer_close_raw(buffer, dst_context); /* Remember buffer */ ((guac_rdp_pointer*) pointer)->layer = buffer; @@ -80,34 +75,45 @@ BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* poi guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - /* Add explicit frame boundaries around cursor set operation if not already - * in a frame (the RDP protocol does not send nor expect frame boundaries - * for cursor changes, but Guacamole does expect this) */ - int in_frame = rdp_client->in_frame; + guac_display_layer* src_layer = ((guac_rdp_pointer*) pointer)->layer; + guac_display_layer_raw_context* src_context = guac_display_layer_open_raw(src_layer); + + guac_display_layer* cursor_layer = guac_display_cursor(rdp_client->display); + + guac_display_layer_resize(cursor_layer, pointer->width, pointer->height); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(cursor_layer); - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 1); + guac_rect ptr_rect = { + .left = 0, + .top = 0, + .right = pointer->width, + .bottom = pointer->height + }; + + guac_rect_constrain(&ptr_rect, &src_context->bounds); + guac_rect_constrain(&ptr_rect, &dst_context->bounds); /* Set cursor */ - guac_common_cursor_set_surface(rdp_client->display->cursor, - pointer->xPos, pointer->yPos, - ((guac_rdp_pointer*) pointer)->layer->surface); + guac_display_layer_raw_context_put(dst_context, &ptr_rect, src_context->buffer, src_context->stride); + dst_context->hint_from = src_layer; + guac_rect_extend(&dst_context->dirty, &ptr_rect); + + guac_display_set_cursor_hotspot(rdp_client->display, pointer->xPos, pointer->yPos); - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); + guac_display_layer_close_raw(cursor_layer, dst_context); + guac_display_layer_close_raw(src_layer, src_context); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } void guac_rdp_pointer_free(rdpContext* context, rdpPointer* pointer) { - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_display_layer* buffer = ((guac_rdp_pointer*) pointer)->layer; + guac_display_layer* buffer = ((guac_rdp_pointer*) pointer)->layer; /* Free buffer */ - guac_common_display_free_buffer(rdp_client->display, buffer); + guac_display_free_layer(buffer); /* NOTE: FreeRDP-allocated memory for the rdpPointer will be automatically * released after this free handler is invoked */ @@ -119,20 +125,10 @@ BOOL guac_rdp_pointer_set_null(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - /* Add explicit frame boundaries around cursor set operation if not already - * in a frame (the RDP protocol does not send nor expect frame boundaries - * for cursor changes, but Guacamole does expect this) */ - int in_frame = rdp_client->in_frame; - - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 1); - /* Set cursor to empty/blank graphic */ - guac_common_cursor_set_blank(rdp_client->display->cursor); - - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_NONE); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } @@ -142,19 +138,9 @@ BOOL guac_rdp_pointer_set_default(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - /* Add explicit frame boundaries around cursor set operation if not already - * in a frame (the RDP protocol does not send nor expect frame boundaries - * for cursor changes, but Guacamole does expect this) */ - int in_frame = rdp_client->in_frame; - - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 1); - /* Set cursor to embedded pointer */ - guac_common_cursor_set_pointer(rdp_client->display->cursor); - - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } diff --git a/src/protocols/rdp/pointer.h b/src/protocols/rdp/pointer.h index 03df2f918..8e8c21e3e 100644 --- a/src/protocols/rdp/pointer.h +++ b/src/protocols/rdp/pointer.h @@ -20,10 +20,9 @@ #ifndef GUAC_RDP_POINTER_H #define GUAC_RDP_POINTER_H -#include "common/display.h" - #include #include +#include #include #ifdef RDP_POINTER_SET_REQUIRES_CONST @@ -45,7 +44,7 @@ typedef struct guac_rdp_pointer { /** * The display layer containing cached image data. */ - guac_common_display_layer* layer; + guac_display_layer* layer; } guac_rdp_pointer; diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index f0ab9b768..a6e7720d3 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -19,7 +19,6 @@ #include "argv.h" #include "beep.h" -#include "bitmap.h" #include "channels/audio-input/audio-buffer.h" #include "channels/audio-input/audio-input.h" #include "channels/cliprdr.h" @@ -32,13 +31,11 @@ #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" -#include "common/cursor.h" -#include "common/display.h" #include "config.h" #include "error.h" #include "fs.h" #include "gdi.h" -#include "glyph.h" +#include "guacamole/display-types.h" #include "keyboard.h" #include "plugins/channels.h" #include "pointer.h" @@ -64,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -198,25 +196,6 @@ static BOOL rdp_freerdp_pre_connect(freerdp* instance) { if (!gdi_init(instance, guac_rdp_get_native_pixel_format(FALSE))) return FALSE; - /* Set up bitmap handling */ - rdpBitmap bitmap = *graphics->Bitmap_Prototype; - bitmap.size = sizeof(guac_rdp_bitmap); - bitmap.New = guac_rdp_bitmap_new; - bitmap.Free = guac_rdp_bitmap_free; - bitmap.Paint = guac_rdp_bitmap_paint; - bitmap.SetSurface = guac_rdp_bitmap_setsurface; - graphics_register_bitmap(graphics, &bitmap); - - /* Set up glyph handling */ - rdpGlyph glyph = *graphics->Glyph_Prototype; - glyph.size = sizeof(guac_rdp_glyph); - glyph.New = guac_rdp_glyph_new; - glyph.Free = guac_rdp_glyph_free; - glyph.Draw = guac_rdp_glyph_draw; - glyph.BeginDraw = guac_rdp_glyph_begindraw; - glyph.EndDraw = guac_rdp_glyph_enddraw; - graphics_register_glyph(graphics, &glyph); - /* Set up pointer handling */ rdpPointer pointer = *graphics->Pointer_Prototype; pointer.size = sizeof(guac_rdp_pointer); @@ -237,18 +216,10 @@ static BOOL rdp_freerdp_pre_connect(freerdp* instance) { GUAC_RDP_CONTEXT(instance)->update->DesktopResize = guac_rdp_gdi_desktop_resize; GUAC_RDP_CONTEXT(instance)->update->BeginPaint = guac_rdp_gdi_begin_paint; GUAC_RDP_CONTEXT(instance)->update->EndPaint = guac_rdp_gdi_end_paint; - GUAC_RDP_CONTEXT(instance)->update->SetBounds = guac_rdp_gdi_set_bounds; GUAC_RDP_CONTEXT(instance)->update->SurfaceFrameMarker = guac_rdp_gdi_surface_frame_marker; GUAC_RDP_CONTEXT(instance)->update->altsec->FrameMarker = guac_rdp_gdi_frame_marker; - rdpPrimaryUpdate* primary = GUAC_RDP_CONTEXT(instance)->update->primary; - primary->DstBlt = guac_rdp_gdi_dstblt; - primary->PatBlt = guac_rdp_gdi_patblt; - primary->ScrBlt = guac_rdp_gdi_scrblt; - primary->MemBlt = guac_rdp_gdi_memblt; - primary->OpaqueRect = guac_rdp_gdi_opaquerect; - /* * If the freerdp instance does not have a LoadChannels callback for * loading plugins we use the PreConnect callback to load plugins instead. @@ -473,7 +444,7 @@ static int rdp_guac_client_wait_for_messages(guac_client* client, GUAC_RDP_MAX_FILE_DESCRIPTORS); /* Wait for data and construct a reasonable frame */ - int result = WaitForMultipleObjects(num_handles, handles, FALSE, + DWORD result = WaitForMultipleObjects(num_handles, handles, FALSE, timeout_msecs); /* Translate WaitForMultipleObjects() return values */ @@ -494,6 +465,30 @@ static int rdp_guac_client_wait_for_messages(guac_client* client, } +/** + * Handles any queued RDP-related events, including inbound RDP messages that + * have been received, updating the Guacamole display accordingly. + * + * @param rdp_client + * The guac_rdp_client of the RDP connection whose current messages should + * be handled. + * + * @return + * True (non-zero) if messages were handled successfully, false (zero) + * otherwise. + */ +static int guac_rdp_handle_events(guac_rdp_client* rdp_client) { + + /* Actually handle messages (this may result in drawing to the + * guac_display, resizing the display buffer, etc.) */ + pthread_mutex_lock(&(rdp_client->message_lock)); + int retval = freerdp_check_event_handles(GUAC_RDP_CONTEXT(rdp_client->rdp_inst)); + pthread_mutex_unlock(&(rdp_client->message_lock)); + + return retval; + +} + /** * Connects to an RDP server as described by the guac_rdp_settings structure * associated with the given client, allocating and freeing all objects @@ -524,15 +519,16 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rwlock_acquire_write_lock(&(rdp_client->lock)); /* Create display */ - rdp_client->display = guac_common_display_alloc(client, - rdp_client->settings->width, - rdp_client->settings->height); + rdp_client->display = guac_display_alloc(client); + + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_resize(default_layer, rdp_client->settings->width, rdp_client->settings->height); /* Use lossless compression only if requested (otherwise, use default * heuristics) */ - guac_common_display_set_lossless(rdp_client->display, settings->lossless); + guac_display_layer_set_lossless(default_layer, settings->lossless); - rdp_client->current_surface = rdp_client->display->default_surface; + rdp_client->current_surface = default_layer; rdp_client->available_svc = guac_common_list_alloc(); @@ -573,7 +569,7 @@ static int guac_rdp_handle_connection(guac_client* client) { settings->server_layout); /* Set default pointer */ - guac_common_cursor_set_pointer(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); /* * Downgrade the lock to allow for concurrent read access. @@ -601,6 +597,8 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rwlock_release_lock(&(rdp_client->lock)); + rdp_client->render_thread = guac_display_render_thread_create(rdp_client->display); + /* Handle messages from RDP server while client is running */ while (client->state == GUAC_CLIENT_RUNNING && !guac_rdp_disp_reconnect_needed(rdp_client->disp)) { @@ -609,62 +607,15 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_disp_update_size(rdp_client->disp, settings, rdp_inst); /* Wait for data and construct a reasonable frame */ - int wait_result = rdp_guac_client_wait_for_messages(client, - GUAC_RDP_FRAME_START_TIMEOUT); - if (wait_result > 0) { - - int processing_lag = guac_client_get_processing_lag(client); - - /* Read server messages until frame is built */ - do { - - guac_timestamp frame_end; - int frame_remaining; - - /* Handle any queued FreeRDP events (this may result in RDP - * messages being sent) */ - pthread_mutex_lock(&(rdp_client->message_lock)); - int event_result = freerdp_check_event_handles(GUAC_RDP_CONTEXT(rdp_inst)); - pthread_mutex_unlock(&(rdp_client->message_lock)); - /* Abort if FreeRDP event handling fails */ - if (!event_result) { - wait_result = -1; - break; - } - - /* Continue handling inbound data if we are in the middle of an RDP frame */ - if (rdp_client->in_frame) { - wait_result = rdp_guac_client_wait_for_messages(client, GUAC_RDP_FRAME_START_TIMEOUT); - if (wait_result >= 0) - continue; - } - - /* Calculate time remaining in frame */ - guac_timestamp frame_start = client->last_sent_timestamp; - frame_end = guac_timestamp_current(); - frame_remaining = frame_start + GUAC_RDP_FRAME_DURATION - - frame_end; - - /* Calculate time that client needs to catch up */ - int time_elapsed = frame_end - frame_start; - int required_wait = processing_lag - time_elapsed; - - /* Increase the duration of this frame if client is lagging */ - if (required_wait > GUAC_RDP_FRAME_TIMEOUT) - wait_result = rdp_guac_client_wait_for_messages(client, - required_wait); - - /* Wait again if frame remaining */ - else if (frame_remaining > 0) - wait_result = rdp_guac_client_wait_for_messages(client, - GUAC_RDP_FRAME_TIMEOUT); - else - break; - - } while (wait_result > 0); + int wait_result = rdp_guac_client_wait_for_messages(client, GUAC_RDP_MESSAGE_CHECK_INTERVAL); + if (wait_result < 0) + break; - } + /* Handle any queued FreeRDP events (this may result in RDP messages + * being sent), aborting later if FreeRDP event handling fails */ + if (!guac_rdp_handle_events(rdp_client)) + wait_result = -1; /* Test whether the RDP server is closing the connection */ int connection_closing; @@ -683,15 +634,6 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE, "Connection closed."); - /* Flush frame only if successful and an RDP frame is not known to be - * in progress */ - else if (!rdp_client->frames_supported || rdp_client->frames_received) { - guac_common_display_flush(rdp_client->display); - guac_client_end_multiple_frames(client, rdp_client->frames_received); - guac_socket_flush(client->socket); - rdp_client->frames_received = 0; - } - } guac_rwlock_acquire_write_lock(&(rdp_client->lock)); @@ -707,9 +649,26 @@ static int guac_rdp_handle_connection(guac_client* client) { freerdp_disconnect(rdp_inst); pthread_mutex_unlock(&(rdp_client->message_lock)); - /* Clean up FreeRDP internal GDI implementation */ + /* Stop render loop */ + guac_display_render_thread_destroy(rdp_client->render_thread); + rdp_client->render_thread = NULL; + + /* Remove reference to FreeRDP's GDI buffer so that it can be safely freed + * prior to freeing the guac_display */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + context->buffer = NULL; + guac_display_layer_close_raw(default_layer, context); + + /* Clean up FreeRDP internal GDI implementation (this must be done BEFORE + * freeing the guac_display, as freeing the GDI will free objects like + * rdpPointer that will attempt to free associated guac_display_layer + * instances during cleanup) */ gdi_free(rdp_inst); + /* Free display */ + guac_display_free(rdp_client->display); + rdp_client->display = NULL; + /* Clean up RDP client context */ freerdp_context_free(rdp_inst); @@ -725,10 +684,6 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_keyboard_free(rdp_client->keyboard); rdp_client->keyboard = NULL; - /* Free display */ - guac_common_display_free(rdp_client->display); - rdp_client->display = NULL; - guac_rwlock_release_lock(&(rdp_client->lock)); /* Client is now disconnected */ diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index c0e9ffacd..76ca18ca3 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -25,9 +25,7 @@ #include "channels/disp.h" #include "channels/rdpei.h" #include "common/clipboard.h" -#include "common/display.h" #include "common/list.h" -#include "common/surface.h" #include "config.h" #include "fs.h" #include "keyboard.h" @@ -45,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -99,31 +98,27 @@ typedef struct guac_rdp_client { /** * The display. */ - guac_common_display* display; + guac_display* display; /** * The surface that GDI operations should draw to. RDP messages exist which * change this surface to allow drawing to occur off-screen. */ - guac_common_surface* current_surface; + guac_display_layer* current_surface; /** - * Whether the RDP server supports defining explicit frame boundaries. + * The current raw context that can be used to draw to Guacamole's default + * layer. This context is obtained prior to FreeRDP manipulation of the GDI + * buffer and closed when FreeRDP is done with the GDI buffer. If no + * drawing to the GDI is currently underway, this will be NULL. */ - int frames_supported; + guac_display_layer_raw_context* current_context; /** - * Whether the RDP server has reported that a new frame is in progress, and - * we are now receiving updates relevant to that frame. + * The current instance of the guac_display render thread. If the thread + * has not yet been started, this will be NULL. */ - int in_frame; - - /** - * The number of distinct frames received from the RDP server since last - * flush, if the RDP server supports reporting frame boundaries. If the RDP - * server does not support tracking frames, this will be zero. - */ - int frames_received; + guac_display_render_thread* render_thread; /** * The current state of the keyboard with respect to the RDP session. diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index 4c62b9e2b..f47b1cff8 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -1774,6 +1774,10 @@ void guac_rdp_push_settings(guac_client* client, rdp_settings->FrameMarkerCommandEnabled = TRUE; rdp_settings->SurfaceFrameMarkerEnabled = TRUE; + /* Always handle input events asynchronously (rather than synchronously + * with the rest of FreeRDP's event loop, including graphics) */ + rdp_settings->AsyncInput = TRUE; + /* Enable RemoteFX / Graphics Pipeline */ if (guac_settings->enable_gfx) { @@ -1803,7 +1807,8 @@ void guac_rdp_push_settings(guac_client* client, /* Client name */ if (guac_settings->client_name != NULL) { - guac_strlcpy(rdp_settings->ClientHostname, guac_settings->client_name, + free(rdp_settings->ClientHostname); + rdp_settings->ClientHostname = guac_strndup(guac_settings->client_name, RDP_CLIENT_HOSTNAME_SIZE); } diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index 5381a9822..5e115ebfd 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -20,8 +20,6 @@ #include "channels/audio-input/audio-input.h" #include "channels/cliprdr.h" #include "channels/pipe-svc.h" -#include "common/cursor.h" -#include "common/display.h" #include "config.h" #include "input.h" #include "rdp.h" @@ -36,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +150,7 @@ int guac_rdp_user_leave_handler(guac_user* user) { /* Update shared cursor state if the display still exists */ if (rdp_client->display != NULL) - guac_common_cursor_remove_user(rdp_client->display->cursor, user); + guac_display_notify_user_left(rdp_client->display, user); /* Free settings if not owner (owner settings will be freed with client) */ if (!user->owner) { diff --git a/src/protocols/ssh/input.c b/src/protocols/ssh/input.c index ba8867b47..f60e1c9fa 100644 --- a/src/protocols/ssh/input.c +++ b/src/protocols/ssh/input.c @@ -19,8 +19,6 @@ #include "config.h" -#include "common/cursor.h" -#include "common/display.h" #include "ssh.h" #include "terminal/terminal.h" diff --git a/src/protocols/ssh/user.c b/src/protocols/ssh/user.c index eff0d8b5d..30b8fe593 100644 --- a/src/protocols/ssh/user.c +++ b/src/protocols/ssh/user.c @@ -21,7 +21,6 @@ #include "argv.h" #include "clipboard.h" -#include "common/display.h" #include "input.h" #include "user.h" #include "pipe.h" diff --git a/src/protocols/vnc/client.c b/src/protocols/vnc/client.c index 2131ae0a6..206dad894 100644 --- a/src/protocols/vnc/client.c +++ b/src/protocols/vnc/client.c @@ -34,6 +34,7 @@ #endif #include +#include #include #include @@ -89,7 +90,7 @@ static int guac_vnc_join_pending_handler(guac_client* client) { /* Synchronize with current display */ if (vnc_client->display != NULL) { - guac_common_display_dup(vnc_client->display, client, broadcast_socket); + guac_display_dup(vnc_client->display, broadcast_socket); guac_socket_flush(broadcast_socket); } @@ -195,7 +196,7 @@ int guac_vnc_client_free_handler(guac_client* client) { /* Free display */ if (vnc_client->display != NULL) - guac_common_display_free(vnc_client->display); + guac_display_free(vnc_client->display); #ifdef ENABLE_PULSE /* If audio enabled, stop streaming */ diff --git a/src/protocols/vnc/client.h b/src/protocols/vnc/client.h index eec4bfce4..86fe264ad 100644 --- a/src/protocols/vnc/client.h +++ b/src/protocols/vnc/client.h @@ -23,25 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The amount of time to wait for new messages from the VNC server before + * moving on to internal matters, in milliseconds. This value must be kept + * reasonably small such that a slow VNC server will not prevent external + * events from being handled (such as the stop signal from guac_client_stop()), + * but large enough that the message handling loop does not eat up CPU + * spinning. */ -#define GUAC_VNC_FRAME_DURATION 40 - -/** - * The amount of time to allow per message read within a frame, in - * milliseconds. If the server is silent for at least this amount of time, the - * frame will be considered finished. - */ -#define GUAC_VNC_FRAME_TIMEOUT 0 - -/** - * The amount of time to wait for a new message from the VNC server when - * beginning a new frame. This value must be kept reasonably small such that - * a slow VNC server will not prevent external events from being handled (such - * as the stop signal from guac_client_stop()), but large enough that the - * message handling loop does not eat up CPU spinning. - */ -#define GUAC_VNC_FRAME_START_TIMEOUT 1000000 +#define GUAC_VNC_MESSAGE_CHECK_INTERVAL 1000 /** * The number of milliseconds to wait between connection attempts. diff --git a/src/protocols/vnc/cursor.c b/src/protocols/vnc/cursor.c index 449fea382..079ddd36f 100644 --- a/src/protocols/vnc/cursor.c +++ b/src/protocols/vnc/cursor.c @@ -20,13 +20,10 @@ #include "config.h" #include "client.h" -#include "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" #include "vnc.h" -#include #include +#include #include #include #include @@ -34,99 +31,96 @@ #include #include -/* Define cairo_format_stride_for_width() if missing */ -#ifndef HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH -#define cairo_format_stride_for_width(format, width) (width*4) -#endif - #include #include #include #include #include -void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp) { +void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int vnc_bpp) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Cairo image buffer */ - int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); - unsigned char* buffer = guac_mem_alloc(h, stride); - unsigned char* buffer_row_current = buffer; + /* Begin drawing operation directly to cursor layer */ + guac_display_layer* cursor_layer = guac_display_cursor(vnc_client->display); + guac_display_layer_resize(cursor_layer, w, h); + guac_display_set_cursor_hotspot(vnc_client->display, x, y); + guac_display_layer_raw_context* context = guac_display_layer_open_raw(cursor_layer); - /* VNC image buffer */ - unsigned int fb_stride = bpp * w; - unsigned char* fb_row_current = client->rcSource; - unsigned char* fb_mask = client->rcMask; + /* Convert operation coordinates to guac_rect for easier manipulation */ + guac_rect op_bounds; + guac_rect_init(&op_bounds, 0, 0, w, h); - int dx, dy; + /* Ensure draw is within current bounds of the pending frame */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Copy image data from VNC client to RGBA buffer */ - for (dy = 0; dyrcSource; + unsigned char* vnc_mask = client->rcMask; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, w); - unsigned int* buffer_current; - unsigned char* fb_current; - - /* Get current buffer row, advance to next */ - buffer_current = (unsigned int*) buffer_row_current; - buffer_row_current += stride; + /* Copy image data from VNC client to RGBA buffer */ + unsigned char* layer_current_row = GUAC_RECT_MUTABLE_BUFFER(op_bounds, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP); + for (int dy = 0; dy < h; dy++) { - /* Get current framebuffer row, advance to next */ - fb_current = fb_row_current; - fb_row_current += fb_stride; + /* Get current Guacamole buffer row, advance to next */ + uint32_t* layer_current_pixel = (uint32_t*) layer_current_row; + layer_current_row += context->stride; - for (dx = 0; dx> client->format.redShift) * 0x100 / (client->format.redMax + 1); - green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax+ 1); - blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); + uint8_t red = (v >> client->format.redShift) * 0x100 / (client->format.redMax + 1); + uint8_t green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax + 1); + uint8_t blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); /* Output ARGB */ if (vnc_client->settings->swap_red_blue) - *(buffer_current++) = (alpha << 24) | (blue << 16) | (green << 8) | red; + *(layer_current_pixel++) = (alpha << 24) | (blue << 16) | (green << 8) | red; else - *(buffer_current++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + *(layer_current_pixel++) = (alpha << 24) | (red << 16) | (green << 8) | blue; - /* Next VNC pixel */ - fb_current += bpp; + /* Advance to next pixel in VNC framebuffer */ + vnc_current_pixel += vnc_bpp; } } - /* Update stored cursor information */ - guac_common_cursor_set_argb(vnc_client->display->cursor, x, y, - buffer, w, h, stride); + /* Mark modified region as dirty */ + guac_rect_extend(&context->dirty, &op_bounds); - /* Free surface */ - guac_mem_free(buffer); + /* Draw operation is now complete */ + guac_display_layer_close_raw(cursor_layer, context); + guac_display_render_thread_notify_modified(vnc_client->render_thread); /* libvncclient does not free rcMask as it does rcSource */ if (client->rcMask != NULL) { free(client->rcMask); client->rcMask = NULL; } + } diff --git a/src/protocols/vnc/cursor.h b/src/protocols/vnc/cursor.h index 193b8749b..18fe7e3cd 100644 --- a/src/protocols/vnc/cursor.h +++ b/src/protocols/vnc/cursor.h @@ -49,10 +49,10 @@ * @param h * The height of the cursor image, in pixels. * - * @param bpp + * @param vnc_bpp * The number of bytes in each pixel, which must be either 4, 2, or 1. */ -void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp); +void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int vnc_bpp); #endif diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 1175e5380..8b09f8b6e 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -22,7 +22,6 @@ #include "client.h" #include "display.h" #include "common/iconv.h" -#include "common/surface.h" #include "vnc.h" #include @@ -50,107 +49,87 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; + guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); - /* Resize the surface if VNC screen size has changed */ - int new_height = client->height; - int old_height = vnc_client->display->default_surface->height; - int new_width = client->width; - int old_width = vnc_client->display->default_surface->width; - if ( - new_height > 0 && new_width > 0 - && (new_height != old_height || new_width != old_width) - ) { - guac_common_surface_resize(vnc_client->display->default_surface, - new_width, new_height); - } - - int dx, dy; + guac_display_layer_raw_context* context = vnc_client->current_context; + unsigned int vnc_bpp = client->format.bitsPerPixel / 8; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, client->width); - /* Cairo image buffer */ - int stride; - unsigned char* buffer; - unsigned char* buffer_row_current; - cairo_surface_t* surface; + /* Convert operation coordinates to guac_rect for easier manipulation */ + guac_rect op_bounds; + guac_rect_init(&op_bounds, x, y, w, h); - /* VNC framebuffer */ - unsigned int bpp; - unsigned int fb_stride; - unsigned char* fb_row_current; + /* Ensure operation bounds are within possibly updated bounds of the + * pending frame (now the RFB client framebuffer) */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Ignore extra update if already handled by copyrect */ - if (vnc_client->copy_rect_used) { - vnc_client->copy_rect_used = 0; - return; - } + /* NOTE: The guac_display will be pointed directly at the libvncclient + * framebuffer if the pixel format used is identical to that expected by + * guac_display. No need to manually copy anything around in that case. */ - /* Init Cairo buffer */ - stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w); - buffer = guac_mem_alloc(h, stride); - buffer_row_current = buffer; + /* All framebuffer formats must be manually converted if not identical to + * the format used by guac_display */ + if (vnc_bpp != GUAC_DISPLAY_LAYER_RAW_BPP || vnc_client->settings->swap_red_blue) { - bpp = client->format.bitsPerPixel/8; - fb_stride = bpp * client->width; - fb_row_current = client->frameBuffer + (y * fb_stride) + (x * bpp); + /* Ensure draw is within current bounds of the pending frame */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Copy image data from VNC client to PNG */ - for (dy = y; dyframeBuffer, vnc_stride, vnc_bpp); + unsigned char* layer_current_row = GUAC_RECT_MUTABLE_BUFFER(op_bounds, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP); + for (int dy = op_bounds.top; dy < op_bounds.bottom; dy++) { - unsigned int* buffer_current; - unsigned char* fb_current; - - /* Get current buffer row, advance to next */ - buffer_current = (unsigned int*) buffer_row_current; - buffer_row_current += stride; + /* Get current Guacamole buffer row, advance to next */ + uint32_t* layer_current_pixel = (uint32_t*) layer_current_row; + layer_current_row += context->stride; - /* Get current framebuffer row, advance to next */ - fb_current = fb_row_current; - fb_row_current += fb_stride; + /* Get current VNC framebuffer row, advance to next */ + const unsigned char* vnc_current_pixel = vnc_current_row; + vnc_current_row += vnc_stride; - for (dx = x; dx> client->format.redShift) * 0x100 / (client->format.redMax + 1); - green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax+ 1); - blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); + /* Translate value to 32-bit RGB */ + uint8_t red = (v >> client->format.redShift) * 0x100 / (client->format.redMax + 1); + uint8_t green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax + 1); + uint8_t blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); - /* Output RGB */ - if (vnc_client->settings->swap_red_blue) - *(buffer_current++) = (blue << 16) | (green << 8) | red; - else - *(buffer_current++) = (red << 16) | (green << 8) | blue; + /* Output RGB */ + if (vnc_client->settings->swap_red_blue) + *(layer_current_pixel++) = 0xFF000000 | (blue << 16) | (green << 8) | red; + else + *(layer_current_pixel++) = 0xFF000000 | (red << 16) | (green << 8) | blue; - fb_current += bpp; + /* Advance to next pixel in VNC framebuffer */ + vnc_current_pixel += vnc_bpp; + } } - } - /* Create surface from decoded buffer */ - surface = cairo_image_surface_create_for_data(buffer, CAIRO_FORMAT_RGB24, - w, h, stride); + } /* end manual convert */ + + /* Mark modified region as dirty */ + guac_rect_extend(&context->dirty, &op_bounds); - /* Draw directly to default layer */ - guac_common_surface_draw(vnc_client->display->default_surface, - x, y, surface); + /* Hint at source of copied data if this update involved CopyRect */ + if (vnc_client->copy_rect_used) { + context->hint_from = default_layer; + vnc_client->copy_rect_used = 0; + } - /* Free surface */ - cairo_surface_destroy(surface); - guac_mem_free(buffer); + guac_display_render_thread_notify_modified(vnc_client->render_thread); } @@ -159,13 +138,12 @@ void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, in guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Copy specified rectangle within default layer */ - guac_common_surface_copy(vnc_client->display->default_surface, - src_x, src_y, w, h, - vnc_client->display->default_surface, dest_x, dest_y); - vnc_client->copy_rect_used = 1; + /* Use original, wrapped proc to perform actual copy between regions of + * libvncclient's display buffer */ + vnc_client->rfb_GotCopyRect(client, src_x, src_y, w, h, dest_x, dest_y); + } #ifdef LIBVNC_HAS_SIZE_MSG @@ -305,17 +283,30 @@ void* guac_vnc_display_set_owner_size(guac_user* owner, void* data) { } -void guac_vnc_display_set_size(rfbClient* client, int width, int height) { +void guac_vnc_display_set_size(rfbClient* client, int requested_width, int requested_height) { /* Get the VNC client */ guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Fit width within bounds, adjusting height to maintain aspect ratio */ - guac_common_display_fit(&width, &height); - - /* Fit height within bounds, adjusting width to maintain aspect ratio */ - guac_common_display_fit(&height, &width); + guac_rect resize = { + .left = 0, + .top = 0, + .right = requested_width, + .bottom = requested_height + }; + + /* Fit width and height within bounds, maintaining aspect ratio */ + guac_rect_shrink(&resize, GUAC_DISPLAY_MAX_WIDTH, GUAC_DISPLAY_MAX_HEIGHT); + int width = guac_rect_width(&resize); + int height = guac_rect_height(&resize); + + if (width <= 0 || height <= 0) { + guac_client_log(gc, GUAC_LOG_WARNING, "Ignoring request to resize " + "desktop to %ix%i as the resulting display would be completely " + "empty", requested_width, requested_height); + return; + } /* Acquire the lock for sending messages to server. */ pthread_mutex_lock(&(vnc_client->message_lock)); @@ -378,11 +369,8 @@ rfbBool guac_vnc_malloc_framebuffer(rfbClient* rfb_client) { guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Resize surface */ - if (vnc_client->display != NULL) - guac_common_surface_resize(vnc_client->display->default_surface, - rfb_client->width, rfb_client->height); - - /* Use original, wrapped proc */ + /* Use original, wrapped proc to resize the buffer maintained by + * libvncclient */ return vnc_client->rfb_MallocFrameBuffer(rfb_client); + } diff --git a/src/protocols/vnc/display.h b/src/protocols/vnc/display.h index a5b56096c..cd7d27b25 100644 --- a/src/protocols/vnc/display.h +++ b/src/protocols/vnc/display.h @@ -22,6 +22,7 @@ #include "config.h" +#include #include #include @@ -109,13 +110,13 @@ void* guac_vnc_display_set_owner_size(guac_user* owner, void* data); * @param display * The VNC client to which the display size update should be sent. * - * @param width + * @param requested_width * The width that is being requested, in pixels. * - * @param height + * @param requested_height * The height that is being requested, in pixels. */ -void guac_vnc_display_set_size(rfbClient* client, int width, int height); +void guac_vnc_display_set_size(rfbClient* client, int requested_width, int requested_height); /** * Sets the pixel format to request of the VNC server. The request will be made diff --git a/src/protocols/vnc/input.c b/src/protocols/vnc/input.c index ec0369381..04c80cf0a 100644 --- a/src/protocols/vnc/input.c +++ b/src/protocols/vnc/input.c @@ -19,11 +19,10 @@ #include "config.h" -#include "common/cursor.h" -#include "common/display.h" #include "display.h" #include "vnc.h" +#include #include #include #include @@ -35,7 +34,7 @@ int guac_vnc_user_mouse_handler(guac_user* user, int x, int y, int mask) { rfbClient* rfb_client = vnc_client->rfb_client; /* Store current mouse location/state */ - guac_common_cursor_update(vnc_client->display->cursor, user, x, y, mask); + guac_display_notify_user_moved_mouse(vnc_client->display, user, x, y, mask); /* Report mouse position within recording */ if (vnc_client->recording != NULL) @@ -79,4 +78,4 @@ int guac_vnc_user_size_handler(guac_user* user, int width, int height) { return 0; } -#endif //LIBVNC_HAS_SIZE_MSG \ No newline at end of file +#endif //LIBVNC_HAS_SIZE_MSG diff --git a/src/protocols/vnc/log.c b/src/protocols/vnc/log.c index ba593e81d..afb93d0e7 100644 --- a/src/protocols/vnc/log.c +++ b/src/protocols/vnc/log.c @@ -21,7 +21,6 @@ #include "client.h" #include "common/iconv.h" -#include "common/surface.h" #include #include diff --git a/src/protocols/vnc/log.h b/src/protocols/vnc/log.h index 65f3dc2d4..3f348605c 100644 --- a/src/protocols/vnc/log.h +++ b/src/protocols/vnc/log.h @@ -24,7 +24,6 @@ #include "client.h" #include "common/iconv.h" -#include "common/surface.h" #include #include diff --git a/src/protocols/vnc/user.c b/src/protocols/vnc/user.c index 0074bbc7a..7025be1f9 100644 --- a/src/protocols/vnc/user.c +++ b/src/protocols/vnc/user.c @@ -21,9 +21,6 @@ #include "clipboard.h" #include "input.h" -#include "common/display.h" -#include "common/dot_cursor.h" -#include "common/pointer_cursor.h" #include "user.h" #include "sftp.h" #include "vnc.h" @@ -35,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -128,10 +126,8 @@ int guac_vnc_user_leave_handler(guac_user* user) { guac_vnc_client* vnc_client = (guac_vnc_client*) user->client->data; - if (vnc_client->display) { - /* Update shared cursor state */ - guac_common_cursor_remove_user(vnc_client->display->cursor, user); - } + if (vnc_client->display) + guac_display_notify_user_left(vnc_client->display, user); /* Free settings if not owner (owner settings will be freed with client) */ if (!user->owner) { diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index d244dcb9f..f72ad7a96 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -23,8 +23,6 @@ #include "client.h" #include "clipboard.h" #include "common/clipboard.h" -#include "common/cursor.h" -#include "common/display.h" #include "cursor.h" #include "display.h" #include "log.h" @@ -42,6 +40,7 @@ #endif #include +#include #include #include #include @@ -140,6 +139,7 @@ rfbClient* guac_vnc_get_client(guac_client* client) { /* Framebuffer update handler */ rfb_client->GotFrameBufferUpdate = guac_vnc_update; + vnc_client->rfb_GotCopyRect = rfb_client->GotCopyRect; rfb_client->GotCopyRect = guac_vnc_copyrect; #ifdef ENABLE_VNC_TLS_LOCKING @@ -252,20 +252,71 @@ rfbClient* guac_vnc_get_client(guac_client* client) { * The rfbClient to wait for. * * @param timeout - * The maximum amount of time to wait, in microseconds. + * The maximum amount of time to wait, in milliseconds. * * @returns * A positive value if data is available, zero if the timeout elapses * before data becomes available, or a negative value on error. */ -static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int timeout) { +static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int msec_timeout) { /* Do not explicitly wait while data is on the buffer */ if (rfb_client->buffered) return 1; /* If no data on buffer, wait for data on socket */ - return WaitForMessage(rfb_client, timeout); + return WaitForMessage(rfb_client, msec_timeout * 1000); + +} + +/** + * Handles any inbound VNC messages that have been received, updating the + * Guacamole display accordingly. + * + * @param vnc_client + * The guac_vnc_client of the VNC connection whose current messages should + * be handled. + * + * @return + * True (non-zero) if messages were handled successfully, false (zero) + * otherwise. + */ +static rfbBool guac_vnc_handle_messages(guac_vnc_client* vnc_client) { + + rfbClient* rfb_client = vnc_client->rfb_client; + guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); + + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + vnc_client->current_context = context; + + /* Actually handle messages (this may result in drawing to the + * guac_display, resizing the display buffer, etc.) */ + rfbBool retval = HandleRFBServerMessage(rfb_client); + + /* Use the buffer of libvncclient directly if it matches the guac_display + * format */ + unsigned int vnc_bpp = rfb_client->format.bitsPerPixel / 8; + if (vnc_bpp == GUAC_DISPLAY_LAYER_RAW_BPP && !vnc_client->settings->swap_red_blue) { + + context->buffer = rfb_client->frameBuffer; + context->stride = guac_mem_ckd_mul_or_die(vnc_bpp, rfb_client->width); + + /* Update bounds of pending frame to match those of RFB framebuffer */ + guac_rect_init(&context->bounds, 0, 0, rfb_client->width, rfb_client->height); + + } + + /* There will be no further drawing operations */ + guac_display_layer_close_raw(default_layer, context); + vnc_client->current_context = NULL; + + /* Resize the surface if VNC screen size has changed (this call + * automatically deals with invalid dimensions and is a no-op + * if the size has not changed) */ + guac_display_layer_resize(default_layer, rfb_client->width, rfb_client->height); + + return retval; } @@ -514,12 +565,13 @@ void* guac_vnc_client_thread(void* data) { } /* Create display */ - vnc_client->display = guac_common_display_alloc(client, - rfb_client->width, rfb_client->height); + vnc_client->display = guac_display_alloc(client); + guac_display_layer_resize(guac_display_default_layer(vnc_client->display), rfb_client->width, rfb_client->height); /* Use lossless compression only if requested (otherwise, use default * heuristics) */ - guac_common_display_set_lossless(vnc_client->display, settings->lossless); + guac_display_layer_set_lossless(guac_display_default_layer(vnc_client->display), + settings->lossless); /* If compression and display quality have been configured, set those. */ if (settings->compress_level >= 0 && settings->compress_level <= 9) @@ -531,10 +583,9 @@ void* guac_vnc_client_thread(void* data) { /* If not read-only, set an appropriate cursor */ if (settings->read_only == 0) { if (settings->remote_cursor) - guac_common_cursor_set_dot(vnc_client->display->cursor); + guac_display_set_cursor(vnc_client->display, GUAC_DISPLAY_CURSOR_DOT); else - guac_common_cursor_set_pointer(vnc_client->display->cursor); - + guac_display_set_cursor(vnc_client->display, GUAC_DISPLAY_CURSOR_POINTER); } #ifdef LIBVNC_HAS_SIZE_MSG @@ -543,77 +594,37 @@ void* guac_vnc_client_thread(void* data) { guac_client_for_owner(client, guac_vnc_display_set_owner_size, rfb_client); #endif // LIBVNC_HAS_SIZE_MSG - guac_socket_flush(client->socket); + guac_display_end_frame(vnc_client->display); - guac_timestamp last_frame_end = guac_timestamp_current(); + vnc_client->render_thread = guac_display_render_thread_create(vnc_client->display); /* Handle messages from VNC server while client is running */ while (client->state == GUAC_CLIENT_RUNNING) { - /* Wait for start of frame */ - int wait_result = guac_vnc_wait_for_messages(rfb_client, - GUAC_VNC_FRAME_START_TIMEOUT); + /* Wait for data and construct a reasonable frame */ + int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_MESSAGE_CHECK_INTERVAL); if (wait_result > 0) { - int processing_lag = guac_client_get_processing_lag(client); - guac_timestamp frame_start = guac_timestamp_current(); - - /* Read server messages until frame is built */ - do { - - guac_timestamp frame_end; - int frame_remaining; - - /* Handle any message received */ - if (!HandleRFBServerMessage(rfb_client)) { - guac_client_abort(client, - GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, - "Error handling message from VNC server."); - break; - } - - /* Calculate time remaining in frame */ - frame_end = guac_timestamp_current(); - frame_remaining = frame_start + GUAC_VNC_FRAME_DURATION - - frame_end; - - /* Calculate time that client needs to catch up */ - int time_elapsed = frame_end - last_frame_end; - int required_wait = processing_lag - time_elapsed; - - /* Increase the duration of this frame if client is lagging */ - if (required_wait > GUAC_VNC_FRAME_TIMEOUT) - wait_result = guac_vnc_wait_for_messages(rfb_client, - required_wait*1000); - - /* Wait again if frame remaining */ - else if (frame_remaining > 0) - wait_result = guac_vnc_wait_for_messages(rfb_client, - GUAC_VNC_FRAME_TIMEOUT*1000); - else - break; - - } while (wait_result > 0); - - /* Record end of frame, excluding server-side rendering time (we - * assume server-side rendering time will be consistent between any - * two subsequent frames, and that this time should thus be - * excluded from the required wait period of the next frame). */ - last_frame_end = frame_start; + /* Handle any message received */ + if (!guac_vnc_handle_messages(vnc_client)) { + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, + "Error handling message from VNC server."); + break; + } } /* If an error occurs, log it and fail */ - if (wait_result < 0) + else if (wait_result < 0) guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Connection closed."); - /* Flush frame */ - guac_common_surface_flush(vnc_client->display->default_surface); - guac_client_end_frame(client); - guac_socket_flush(client->socket); - } + /* Stop render loop */ + guac_display_render_thread_destroy(vnc_client->render_thread); + vnc_client->render_thread = NULL; + /* Kill client and finish connection */ guac_client_stop(client); guac_client_log(client, GUAC_LOG_INFO, "Internal VNC client disconnected"); diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index 16fcb0e4a..6b83d41d0 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -23,13 +23,12 @@ #include "config.h" #include "common/clipboard.h" -#include "common/display.h" #include "common/iconv.h" -#include "common/surface.h" #include "display.h" #include "settings.h" #include +#include #include #include @@ -86,6 +85,12 @@ typedef struct guac_vnc_client { */ MallocFrameBufferProc rfb_MallocFrameBuffer; + /** + * The original CopyRect processing procedure provided by the initialized + * rfbClient. + */ + GotCopyRectProc rfb_GotCopyRect; + /** * Whether copyrect was used to produce the latest update received * by the VNC server. @@ -100,7 +105,19 @@ typedef struct guac_vnc_client { /** * The current display state. */ - guac_common_display* display; + guac_display* display; + + /** + * The context of the current drawing (update) operation, if any. If no + * operation is in progress, this will be NULL. + */ + guac_display_layer_raw_context* current_context; + + /** + * The current instance of the guac_display render thread. If the thread + * has not yet been started, this will be NULL. + */ + guac_display_render_thread* render_thread; /** * Internal clipboard. diff --git a/src/terminal/buffer.c b/src/terminal/buffer.c index fe93fc4b6..e558d0345 100644 --- a/src/terminal/buffer.c +++ b/src/terminal/buffer.c @@ -19,13 +19,90 @@ #include "terminal/buffer.h" #include "terminal/common.h" +#include "terminal/terminal.h" +#include #include +#include #include #include -guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* default_character) { +/** + * The minimum number of columns to allocate for a buffer row, regardless of + * the terminal size. We set a minimum size here to reduce the memory + * reallocation overhead for small rows. + */ +#define GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE 256 + +/** + * A single variable-length row of terminal data. + */ +typedef struct guac_terminal_buffer_row { + + /** + * Array of guac_terminal_char representing the contents of the row. + */ + guac_terminal_char* characters; + + /** + * The length of this row in characters. This is the number of initialized + * characters in the buffer, usually equal to the number of characters + * in the screen width at the time this row was created. + */ + unsigned int length; + + /** + * The number of elements in the characters array. After the length + * equals this value, the array must be resized. + */ + unsigned int available; + + /** + * True if the current row has been wrapped to avoid going off the screen. + * False otherwise. + */ + bool wrapped_row; + +} guac_terminal_buffer_row; + +struct guac_terminal_buffer { + + /** + * The character to assign to newly-allocated cells. + */ + guac_terminal_char default_character; + + /** + * Array of buffer rows. This array functions as a ring buffer. + * When a new row needs to be appended, the top reference is moved down + * and the old top row is replaced. + */ + guac_terminal_buffer_row* rows; + + /** + * The index of the first row in the buffer (the row which represents row 0 + * with respect to the terminal display). This is also the index of the row + * to replace when insufficient space remains in the buffer to add a new + * row. + */ + unsigned int top; + + /** + * The number of rows currently stored in the buffer. + */ + unsigned int length; + + /** + * The number of rows in the buffer. This is the total capacity + * of the buffer. + */ + unsigned int available; + +}; + +guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, + const guac_terminal_char* default_character) { /* Allocate scrollback */ guac_terminal_buffer* buffer = @@ -46,7 +123,7 @@ guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* d for (i=0; iavailable = 256; + row->available = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE; row->length = 0; row->wrapped_row = false; row->characters = guac_mem_alloc(sizeof(guac_terminal_char), row->available); @@ -77,66 +154,231 @@ void guac_terminal_buffer_free(guac_terminal_buffer* buffer) { } -guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row, int width) { +void guac_terminal_buffer_reset(guac_terminal_buffer* buffer) { + buffer->top = 0; + buffer->length = 0; +} - int i; - guac_terminal_char* first; - guac_terminal_buffer_row* buffer_row; +/** + * Returns the row at the given location. The row returned is guaranteed to be at least the given + * width. + * + * @param buffer + * The buffer to retrieve a row from. + * + * @param row + * The index of the row to retrieve, where zero is the top-most row. + * Negative indices represent rows in the scrollback buffer, above the + * top-most row. + * + * @return + * The buffer row at the given location, or NULL if there is no such row. + */ +static guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row) { + + if (abs(row) >= buffer->available) + return NULL; /* Normalize row index into a scrollback buffer index */ - int index = (buffer->top + row) % buffer->available; - if (index < 0) - index += buffer->available; + unsigned int index = (buffer->top + row) % buffer->available; + return &(buffer->rows[index]); - /* Get row */ - buffer_row = &(buffer->rows[index]); +} + +/** + * Rounds the given value up to the nearest possible row length. To avoid + * unnecessary, repeated resizing of rows, each row length is rounded up to the + * nearest power of two. + * + * @param value + * The value to round. + * + * @return + * The power of two that is closest to the given value without exceeding + * that value. + */ +static unsigned int guac_terminal_buffer_row_length(int value) { + + GUAC_ASSERT(value >= 0); + GUAC_ASSERT(value <= GUAC_TERMINAL_MAX_COLUMNS); + + unsigned int rounded = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE; + while (rounded < value) + rounded <<= 1; + + return rounded; + +} + +/** + * Expands the amount of space allocated for the given row such that it + * may contain at least the given number of characters, if possible. If the row + * cannot be expanded due to buffer size limitations, it will be expanded to + * the greatest size allowed without exceeding those limits. + * + * @param row + * The row to expand. + * + * @param length + * The number of characters that the row must be able to store. + * + * @param default_character + * The character that should fill any newly-allocated character cells. + */ +static void guac_terminal_buffer_row_expand(guac_terminal_buffer_row* row, int length, + const guac_terminal_char* default_character) { + + /* Bail out if no resize/init is necessary */ + if (length <= row->length) + return; - /* If resizing is needed */ - if (width >= buffer_row->length) { + /* Limit maximum possible row size to the limits of the terminal display */ + if (length > GUAC_TERMINAL_MAX_COLUMNS) + length = GUAC_TERMINAL_MAX_COLUMNS; - /* Expand if necessary */ - if (width > buffer_row->available) { - buffer_row->available = guac_mem_ckd_mul_or_die(width, 2); - buffer_row->characters = guac_mem_realloc_or_die(buffer_row->characters, - sizeof(guac_terminal_char), buffer_row->available); + /* Expand allocated memory if there is otherwise insufficient space to fit + * the provided length */ + if (length > row->available) { + row->available = guac_terminal_buffer_row_length(length); + row->characters = guac_mem_realloc_or_die(row->characters, + sizeof(guac_terminal_char), row->available); + } + + /* Initialize new part of row */ + for (int i = row->length; i < row->available; i++) + row->characters[i] = *default_character; + + row->length = length; + +} + +/** + * Enforces a character break at the given edge, ensuring that the left side + * of the edge is the final column of a character, and the right side of the + * edge is the initial column of a DIFFERENT character. + * + * @param buffer + * The buffer containing the character. + * + * @param row + * The row index of the row containing the character. + * + * @param edge + * The relative edge number where a break is required. For a character in + * column N, that character's left edge is N and the right edge is N+1. + */ +static void guac_terminal_buffer_force_break(guac_terminal_buffer* buffer, int row, int edge) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + /* Ensure character to left of edge is unbroken */ + if (edge > 0) { + + int end_column = edge - 1; + int start_column = end_column; + + guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + + /* Determine start column */ + while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { + start_char--; + start_column--; + } + + /* Advance to start of broken character if necessary */ + if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { + start_column += start_char->width; + start_char += start_char->width; } - /* Initialize new part of row */ - first = &(buffer_row->characters[buffer_row->length]); - for (i=buffer_row->length; idefault_character; + /* Clear character if broken */ + if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { + + guac_terminal_char cleared_char; + cleared_char.value = ' '; + cleared_char.attributes = start_char->attributes; + cleared_char.width = 1; - buffer_row->length = width; + guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char); + + } } - /* Return found row */ - return buffer_row; + /* Ensure character to right of edge is unbroken */ + if (edge >= 0 && edge < buffer_row->length) { + + int start_column = edge; + int end_column = start_column; + + guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + guac_terminal_char* end_char = &(buffer_row->characters[end_column]); + + /* Determine end column */ + while (end_column+1 < buffer_row->length && (end_char+1)->value == GUAC_CHAR_CONTINUATION) { + end_char++; + end_column++; + } + + /* Advance to start of broken character if necessary */ + if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { + start_column += start_char->width; + start_char += start_char->width; + } + + /* Clear character if broken */ + if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { + + guac_terminal_char cleared_char; + cleared_char.value = ' '; + cleared_char.attributes = start_char->attributes; + cleared_char.width = 1; + + guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char); + + } + + } } void guac_terminal_buffer_copy_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, int offset) { - guac_terminal_char* src; - guac_terminal_char* dst; - /* Get row */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row, end_column + offset + 1); + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; - /* Fit range within bounds */ - start_column = guac_terminal_fit_to_range(start_column, 0, buffer_row->length - 1); - end_column = guac_terminal_fit_to_range(end_column, 0, buffer_row->length - 1); - start_column = guac_terminal_fit_to_range(start_column + offset, 0, buffer_row->length - 1) - offset; - end_column = guac_terminal_fit_to_range(end_column + offset, 0, buffer_row->length - 1) - offset; + guac_terminal_buffer_row_expand(buffer_row, end_column + offset + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length >= end_column + offset + 1); + + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_column = guac_terminal_fit_to_range(start_column, 0, buffer_row->length - offset - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - offset - 1); + } + else { + start_column = guac_terminal_fit_to_range(start_column, -offset, buffer_row->length - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - 1); + } /* Determine source and destination locations */ - src = &(buffer_row->characters[start_column]); - dst = &(buffer_row->characters[start_column + offset]); + guac_terminal_char* src = &(buffer_row->characters[start_column]); + guac_terminal_char* dst = &(buffer_row->characters[start_column + offset]); /* Copy data */ memmove(dst, src, sizeof(guac_terminal_char) * (end_column - start_column + 1)); + /* Force breaks around destination region */ + guac_terminal_buffer_force_break(buffer, row, start_column + offset); + guac_terminal_buffer_force_break(buffer, row, end_column + offset + 1); + } void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, @@ -161,11 +403,17 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, for (i = start_row; i <= end_row; i++) { /* Get source and destination rows */ - guac_terminal_buffer_row* src_row = guac_terminal_buffer_get_row(buffer, current_row, 0); - guac_terminal_buffer_row* dst_row = guac_terminal_buffer_get_row(buffer, current_row + offset, src_row->length); + guac_terminal_buffer_row* src_row = guac_terminal_buffer_get_row(buffer, current_row); + guac_terminal_buffer_row* dst_row = guac_terminal_buffer_get_row(buffer, current_row + offset); + + if (src_row == NULL || dst_row == NULL) + continue; + + guac_terminal_buffer_row_expand(dst_row, src_row->length, &buffer->default_character); + GUAC_ASSERT(dst_row->length >= src_row->length); /* Copy data */ - memcpy(dst_row->characters, src_row->characters, sizeof(guac_terminal_char) * src_row->length); + memcpy(dst_row->characters, src_row->characters, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_char), src_row->length)); dst_row->length = src_row->length; dst_row->wrapped_row = src_row->wrapped_row; @@ -179,40 +427,140 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, } +void guac_terminal_buffer_scroll_up(guac_terminal_buffer* buffer, int amount) { + + if (amount <= 0) + return; + + buffer->top = (buffer->top + amount) % buffer->available; + + buffer->length += amount; + if (buffer->length > buffer->available) + buffer->length = buffer->available; + +} + +void guac_terminal_buffer_scroll_down(guac_terminal_buffer* buffer, int amount) { + + if (amount <= 0) + return; + + buffer->top = (buffer->top - amount) % buffer->available; + +} + +unsigned int guac_terminal_buffer_get_columns(guac_terminal_buffer* buffer, + guac_terminal_char** characters, bool* is_wrapped, int row) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return 0; + + if (characters != NULL) + *characters = buffer_row->characters; + + if (is_wrapped != NULL) + *is_wrapped = buffer_row->wrapped_row; + + return buffer_row->length; + +} + void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, guac_terminal_char* character) { - int i, j; - guac_terminal_char* current; + /* Do nothing if there's nothing to do (glyph is empty) or if nothing + * sanely can be done (row is impossibly large or glyph has an invalid + * width) */ + if (character->width <= 0 || row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS) + return; - /* Do nothing if glyph is empty */ - if (character->width == 0) + /* Do nothing if there is no such row within the buffer (the given row index + * does not refer to an actual row, even considering scrollback) */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) return; /* Build continuation char (for multicolumn characters) */ - guac_terminal_char continuation_char; - continuation_char.value = GUAC_CHAR_CONTINUATION; - continuation_char.attributes = character->attributes; - continuation_char.width = 0; /* Not applicable for GUAC_CHAR_CONTINUATION */ + guac_terminal_char continuation_char = { + .value = GUAC_CHAR_CONTINUATION, + .attributes = character->attributes, + .width = 0 /* Not applicable for GUAC_CHAR_CONTINUATION */ + }; - /* Get and expand row */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row, end_column+1); + start_column = guac_terminal_fit_to_range(start_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); + end_column = guac_terminal_fit_to_range(end_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); - /* Set values */ - current = &(buffer_row->characters[start_column]); - for (i = start_column; i <= end_column; i += character->width) { + guac_terminal_buffer_row_expand(buffer_row, end_column + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length >= end_column + 1); - *(current++) = *character; + int remaining_continuation_chars = 0; + for (int i = start_column; i <= end_column; i++) { /* Store any required continuation characters */ - for (j=1; j < character->width; j++) - *(current++) = continuation_char; + if (remaining_continuation_chars > 0) { + buffer_row->characters[i] = continuation_char; + remaining_continuation_chars--; + } + else { + buffer_row->characters[i] = *character; + remaining_continuation_chars = character->width - 1; + } } /* Update length depending on row written */ if (character->value != 0 && row >= buffer->length) - buffer->length = row+1; + buffer->length = row + 1; + + /* Force breaks around destination region */ + guac_terminal_buffer_force_break(buffer, row, start_column); + guac_terminal_buffer_force_break(buffer, row, end_column + 1); + +} + +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor) { + + /* Do if nothing sanely can be done (row is impossibly large) */ + if (row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS) + return; + + /* Do nothing if there is no such row within the buffer (the given row index + * does not refer to an actual row, even considering scrollback) */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + column = guac_terminal_fit_to_range(column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); + + guac_terminal_buffer_row_expand(buffer_row, column + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length > column + 1); + + buffer_row->characters[column].attributes.cursor = is_cursor; + +} + +unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback) { + + /* If the buffer contains more rows than requested, pretend it only + * contains the requested number of rows */ + unsigned int effective_length = buffer->length; + if (effective_length > scrollback) + effective_length = scrollback; + + return effective_length; + +} + + +void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + buffer_row->wrapped_row = wrapped; } diff --git a/src/terminal/common.c b/src/terminal/common.c index 5f5509dbe..3b83d4c82 100644 --- a/src/terminal/common.c +++ b/src/terminal/common.c @@ -17,13 +17,20 @@ * under the License. */ +#include "terminal/common.h" #include "terminal/types.h" +#include + #include #include int guac_terminal_fit_to_range(int value, int min, int max) { + /* This should never happen outside a logic error in the caller, but best + * to bail out here where debugging will be less onerous */ + GUAC_ASSERT(min <= max); + if (value < min) return min; if (value > max) return max; diff --git a/src/terminal/display.c b/src/terminal/display.c index febad15ec..402b849dc 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -348,39 +349,52 @@ int guac_terminal_display_lookup_color(guac_terminal_display* display, void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, int start_column, int end_column, int offset) { - int i; - guac_terminal_operation* src_current; - guac_terminal_operation* current; - /* Ignore operations outside display bounds */ if (row < 0 || row >= display->height) return; - /* Fit range within bounds */ - start_column = guac_terminal_fit_to_range(start_column, 0, display->width - 1); - end_column = guac_terminal_fit_to_range(end_column, 0, display->width - 1); - start_column = guac_terminal_fit_to_range(start_column + offset, 0, display->width - 1) - offset; - end_column = guac_terminal_fit_to_range(end_column + offset, 0, display->width - 1) - offset; + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_column = guac_terminal_fit_to_range(start_column, 0, display->width - offset - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - offset - 1); + } + else { + start_column = guac_terminal_fit_to_range(start_column, -offset, display->width - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - 1); + } - src_current = &(display->operations[row * display->width + start_column]); - current = &(display->operations[row * display->width + start_column + offset]); + /* Determine source and destination locations */ - /* Move data */ - memmove(current, src_current, - (end_column - start_column + 1) * sizeof(guac_terminal_operation)); + size_t row_offset = guac_mem_ckd_mul_or_die(row, display->width); + size_t src_offset = guac_mem_ckd_add_or_die(row_offset, start_column); + + size_t dst_offset; + if (offset >= 0) + dst_offset = guac_mem_ckd_add_or_die(src_offset, offset); + else + dst_offset = guac_mem_ckd_sub_or_die(src_offset, -offset); + + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); + + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), (end_column - start_column + 1))); /* Update operations */ - for (i=start_column; i<=end_column; i++) { + for (int column = start_column; column <= end_column; column++) { /* If no operation here, set as copy */ - if (current->type == GUAC_CHAR_NOP) { - current->type = GUAC_CHAR_COPY; - current->row = row; - current->column = i; + if (dst->type == GUAC_CHAR_NOP) { + dst->type = GUAC_CHAR_COPY; + dst->row = row; + dst->column = column; } /* Next column */ - current++; + dst++; } @@ -389,28 +403,42 @@ void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, void guac_terminal_display_copy_rows(guac_terminal_display* display, int start_row, int end_row, int offset) { - int row, col; - guac_terminal_operation* src_current_row; - guac_terminal_operation* current_row; + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_row = guac_terminal_fit_to_range(start_row, 0, display->height - offset - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - offset - 1); + } + else { + start_row = guac_terminal_fit_to_range(start_row, -offset, display->height - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - 1); + } - /* Fit range within bounds */ - start_row = guac_terminal_fit_to_range(start_row, 0, display->height - 1); - end_row = guac_terminal_fit_to_range(end_row, 0, display->height - 1); - start_row = guac_terminal_fit_to_range(start_row + offset, 0, display->height - 1) - offset; - end_row = guac_terminal_fit_to_range(end_row + offset, 0, display->height - 1) - offset; + /* Determine source and destination locations */ + + size_t dst_start_row; + if (offset >= 0) + dst_start_row = guac_mem_ckd_add_or_die(start_row, offset); + else + dst_start_row = guac_mem_ckd_sub_or_die(start_row, -offset); - src_current_row = &(display->operations[start_row * display->width]); - current_row = &(display->operations[(start_row + offset) * display->width]); + size_t src_offset = guac_mem_ckd_mul_or_die(start_row, display->width); + size_t dst_offset = guac_mem_ckd_mul_or_die(dst_start_row, display->width); - /* Move data */ - memmove(current_row, src_current_row, - (end_row - start_row + 1) * sizeof(guac_terminal_operation) * display->width); + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); + + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), + display->width, (end_row - start_row + 1))); /* Update operations */ - for (row=start_row; row<=end_row; row++) { + for (int row = start_row; row <= end_row; row++) { - guac_terminal_operation* current = current_row; - for (col=0; colwidth; col++) { + guac_terminal_operation* current = dst; + for (int col = 0; col < display->width; col++) { /* If no operation here, set as copy */ if (current->type == GUAC_CHAR_NOP) { @@ -425,7 +453,7 @@ void guac_terminal_display_copy_rows(guac_terminal_display* display, } /* Next row */ - current_row += display->width; + dst += display->width; } @@ -434,9 +462,6 @@ void guac_terminal_display_copy_rows(guac_terminal_display* display, void guac_terminal_display_set_columns(guac_terminal_display* display, int row, int start_column, int end_column, guac_terminal_char* character) { - int i; - guac_terminal_operation* current; - /* Do nothing if glyph is empty */ if (character->width == 0) return; @@ -449,10 +474,11 @@ void guac_terminal_display_set_columns(guac_terminal_display* display, int row, start_column = guac_terminal_fit_to_range(start_column, 0, display->width - 1); end_column = guac_terminal_fit_to_range(end_column, 0, display->width - 1); - current = &(display->operations[row * display->width + start_column]); + size_t start_offset = guac_mem_ckd_add_or_die(guac_mem_ckd_mul_or_die(row, display->width), start_column); + guac_terminal_operation* current = &(display->operations[start_offset]); /* For each column in range */ - for (i = start_column; i <= end_column; i += character->width) { + for (int col = start_column; col <= end_column; col += character->width) { /* Set operation */ current->type = GUAC_CHAR_SET; @@ -471,8 +497,8 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int if (width == display->width && height == display->height) return; - guac_terminal_operation* current; - int x, y; + GUAC_ASSERT(width >= 0 && width <= GUAC_TERMINAL_MAX_COLUMNS); + GUAC_ASSERT(height >= 0 && height <= GUAC_TERMINAL_MAX_ROWS); /* Fill with background color */ guac_terminal_char fill = { @@ -493,11 +519,11 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int sizeof(guac_terminal_operation)); /* Init each operation buffer row */ - current = display->operations; - for (y=0; yoperations; + for (int y = 0; y < height; y++) { /* Init entire row to NOP */ - for (x=0; xwidth && y < display->height) diff --git a/src/terminal/select.c b/src/terminal/select.c index 20bf9b5d2..406a0c43f 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -146,13 +146,14 @@ void guac_terminal_select_redraw(guac_terminal* terminal) { static int guac_terminal_find_char(guac_terminal* terminal, int row, int* column) { + guac_terminal_char* characters; int start_column = *column; - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - if (start_column < buffer_row->length) { + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + if (start_column >= 0 && start_column < length) { /* Find beginning of character */ - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + guac_terminal_char* start_char = &(characters[start_column]); while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { start_char--; start_column--; @@ -251,21 +252,21 @@ void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { } /** - * Appends the text within the given subsection of a terminal row to the + * Appends the text within the given array of terminal characters to the * clipboard. The provided coordinates are considered inclusively (the - * characters at the start and end column are included in the copied - * text). Any out-of-bounds coordinates will be automatically clipped within - * the bounds of the given row. + * characters at the start and end column are included in the copied text). Any + * out-of-bounds coordinates will be automatically clipped within the bounds of + * the given array. * * @param terminal * The guac_terminal instance associated with the buffer containing the * text being copied and the clipboard receiving the copied text. * - * @param row - * The row number of the text within the terminal to be copied into the - * clipboard, where the first (top-most) row in the terminal is row 0. Rows - * within the scrollback buffer (above the top-most row of the terminal) - * will be negative. + * @param characters + * The array of characters copied into the clipboard. + * + * @param length + * The number of characters in the provided character array. * * @param start * The first column of the text to be copied from the given row into the @@ -275,47 +276,40 @@ void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { * @param end * The last column of the text to be copied from the given row into the * clipboard associated with the given terminal, where 0 is the first - * (left-most) column within the row, or a negative value to denote that - * the last column in the row should be used. + * (left-most) column within the row. */ -static void guac_terminal_clipboard_append_row(guac_terminal* terminal, - int row, int start, int end) { +static void guac_terminal_clipboard_append_characters(guac_terminal* terminal, + guac_terminal_char* characters, unsigned int length, int start, int end) { char buffer[1024]; - int i = start; int eol; - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); - /* If selection is entirely outside the bounds of the row, then there is * nothing to append */ - if (start < 0 || start > buffer_row->length - 1) + if (start < 0 || end < 0 || start >= length) return; - /* Clip given range to actual bounds of row */ - if (end < 0 || end > buffer_row->length - 1) - end = buffer_row->length - 1; + /* Ensure desired end column is within bounds */ + if (end >= length) + end = length - 1; /* Get position of last not null char */ - for (eol = buffer_row->length - 1; eol > start; eol--) { - - if (buffer_row->characters[eol].value != 0) + for (eol = end; eol > start; eol--) { + if (characters[eol].value != 0) break; - } /* Repeatedly convert chunks of terminal buffer rows until entire specified * region has been appended to clipboard */ - while (i <= end) { + for (int i = start; i <= end;) { int remaining = sizeof(buffer); char* current = buffer; /* Convert as many codepoints within the given range as possible */ - for (i = start; i <= end; i++) { + for (; i <= end; i++) { - int codepoint = buffer_row->characters[i].value; + int codepoint = characters[i].value; /* Fill empty with spaces if not at end of line */ if (codepoint == 0 && i < eol) @@ -364,43 +358,27 @@ void guac_terminal_select_end(guac_terminal* terminal) { guac_terminal_select_normalized_range(terminal, &start_row, &start_col, &end_row, &end_col); - /* If only one row, simply copy */ - if (end_row == start_row) - guac_terminal_clipboard_append_row(terminal, start_row, start_col, end_col); - - /* Otherwise, copy multiple rows */ - else { - - /* Get the first selected row */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, start_row, 0); - - /* Store first row from start_col to last available col */ - guac_terminal_clipboard_append_row(terminal, start_row, start_col, -1); - - /* Store all middle rows */ - for (int row = start_row + 1; row < end_row; row++) { + guac_terminal_char* characters; + bool last_row_was_wrapped = true; - /* Add a new line only if the line was not wrapped */ - if (buffer_row->wrapped_row == false) - guac_common_clipboard_append(terminal->clipboard, "\n", 1); - - /* Store middle row */ - guac_terminal_clipboard_append_row(terminal, row, 0, -1); - - /* Get next buffer row */ - buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - } + for (int row = start_row; row <= end_row; row++) { - /* Add a new line only if the line was not wrapped */ - if (buffer_row->wrapped_row == false) + /* Add a newline only if the previous line was not wrapped */ + if (!last_row_was_wrapped) guac_common_clipboard_append(terminal->clipboard, "\n", 1); - /* Store last row from col 0 to end_col */ - guac_terminal_clipboard_append_row(terminal, end_row, 0, end_col); + /* Append next row from desired region, adjusting the start/end column + * to account for selections that start or end in the middle of a row. + * With the exception of the start and end rows, all other rows are + * copied in their entirety. */ + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &last_row_was_wrapped, row); + guac_terminal_clipboard_append_characters(terminal, characters, length, + (row == start_row) ? start_col : 0, + (row == end_row) ? end_col : length - 1); } - /* Send data */ + /* Broadcast copied data to all connected users only if allowed */ if (!terminal->disable_copy) { guac_common_clipboard_send(terminal->clipboard, client); guac_socket_flush(socket); diff --git a/src/terminal/terminal-handlers.c b/src/terminal/terminal-handlers.c index a3756abae..f139c1090 100644 --- a/src/terminal/terminal-handlers.c +++ b/src/terminal/terminal-handlers.c @@ -51,9 +51,105 @@ #define GUAC_TERMINAL_OK "\x1B[0n" /** - * Alternative buffer CSI sequence. + * Flag number for the DEC Private Mode Set (DECSET) operation that switches + * from the normal buffer to the alternate buffer. + * + * NOTE: Switching to the alternate buffer is common for text editors that wish + * to present a user interface yet preserve the original contents of the + * terminal. + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER 47 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that switches + * from the normal buffer to the alternate buffer AND clears the alternate + * buffer (but only if the alternate buffer wasn't already selected). + * + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR 1047 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that saves the + * current cursor location. The cursor location can later be restored through + * GUAC_TERMINAL_DECRST_RESTORE_CURSOR and related operations. + * + * @see GUAC_TERMINAL_DECRST_RESTORE_CURSOR + * @see GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR + */ +#define GUAC_TERMINAL_DECSET_SAVE_CURSOR 1048 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that saves the + * current cursor location, switches to the alternate buffer, AND clears the + * alternate buffer if the alternate buffer was not already selected. + * + * This is effectively a combination of GUAC_TERMINAL_DECSET_SAVE_CURSOR and + * GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR 1049 + +/** + * Flag number for the DEC Private Mode Reset (DECRST) operation that switches + * from the alternate buffer to the normal buffer. + * + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER */ -#define GUAC_TERMINAL_ALT_BUFFER 1049 +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER 47 + +/** + * Flag number for the DEC Private Mode Reset (DECRST) operation that switches + * from the alternate buffer to the normal buffer AND clears the normal buffer + * (but only if the normal buffer wasn't already selected). + * + * @see GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER + */ +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR 1047 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that restores + * the cursor location. The cursor location must have been previously saved + * through GUAC_TERMINAL_DECSET_SAVE_CURSOR or related operations. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + */ +#define GUAC_TERMINAL_DECRST_RESTORE_CURSOR 1048 + +/** + * Flag number for the DEC Private Mode Reet (DECRST) operation that and + * restores the previously saved cursor location. The normal buffer is not + * cleared. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + */ +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR 1049 + +/** + * Parses a numeric terminal parameter, as may be accepted by console code + * sequences in general. Only integers between 0 and INT_MAX are parsed, + * inclusive. If a value cannot be parsed, 0 is returned. + * + * @param str + * The terminal parameter to parse, as a null-terminated string. + * + * @return + * The integer value of the provided string, or 0 if the string cannot be + * parsed for any reason. + */ +static int guac_terminal_parse_numeric_param(const char* str) { + + errno = 0; + unsigned long value = strtoul(str, NULL, 10); + if (errno || value > INT_MAX) + return 0; + + return value; + +} /** * Advances the cursor to the next row, scrolling if the cursor would otherwise @@ -68,10 +164,8 @@ */ static void guac_terminal_linefeed(guac_terminal* term, bool force_wrap) { - /* Assign in wrapped_row: 1 to avoid \n in clipboard or 0 to add \n */ - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(term->buffer, term->cursor_row, 0); - buffer_row->wrapped_row = force_wrap; + /* Pass through possible change in whether the current row was wrapped */ + guac_terminal_buffer_set_wrapped(term->current_buffer, term->cursor_row, force_wrap); /* Scroll up if necessary */ if (term->cursor_row == term->scroll_end) @@ -637,7 +731,7 @@ int guac_terminal_csi(guac_terminal* term, unsigned char c) { /* Finish parameter */ argv_buffer[argv_length] = 0; - argv[argc++] = atoi(argv_buffer); + argv[argc++] = guac_terminal_parse_numeric_param(argv_buffer); /* Prepare for next parameter */ argv_length = 0; @@ -918,31 +1012,87 @@ int guac_terminal_csi(guac_terminal* term, unsigned char c) { break; - /* h: Set Mode */ + /* h: Set Mode (DECSET) */ case 'h': - - /* Look up flag and set */ + + /* Save cursor for later restoration */ + if (argv[0] == GUAC_TERMINAL_DECSET_SAVE_CURSOR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + term->saved_cursor_row = term->cursor_row; + term->saved_cursor_col = term->cursor_col; + } + + if (term->current_buffer != term->alternate_buffer) { + + /* Switch to alternate buffer */ + if (argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + + term->current_buffer = term->alternate_buffer; + + /* Update scrollbar bounds (the different buffers have differing levels of scrollback) */ + guac_terminal_scrollbar_set_bounds(term->scrollbar, + -guac_terminal_get_available_scroll(term), 0); + + } + + /* Clear alternate buffer only if we were previously using + * the normal buffer */ + if (argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + guac_terminal_clear_range(term, + 0, 0, term->term_height - 1, term->term_width - 1); + } + + } + + /* Look up flag and set */ flag = __guac_terminal_get_flag(term, argv[0], private_mode_character); if (flag != NULL) *flag = true; - /* Open alternate screen buffer */ - if (argv[0] == GUAC_TERMINAL_ALT_BUFFER) - guac_terminal_switch_buffers(term, true); - break; - /* l: Reset Mode */ + /* l: Reset Mode (DECRST) */ case 'l': - - /* Look up flag and clear */ + + if (term->current_buffer != term->normal_buffer) { + + /* Switch back to normal buffer */ + if (argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR) { + + term->current_buffer = term->normal_buffer; + + /* Update scrollbar bounds (the different buffers have differing levels of scrollback) */ + guac_terminal_scrollbar_set_bounds(term->scrollbar, + -guac_terminal_get_available_scroll(term), 0); + + } + + /* Clear normal buffer only if we were previously using + * the alternate buffer */ + if (argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR) { + guac_terminal_clear_range(term, + 0, 0, term->term_height - 1, term->term_width - 1); + } + + } + + /* Restore previously saved cursor */ + if (argv[0] == GUAC_TERMINAL_DECRST_RESTORE_CURSOR + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR) { + guac_terminal_move_cursor(term, + term->saved_cursor_row, + term->saved_cursor_col); + } + + /* Look up flag and clear */ flag = __guac_terminal_get_flag(term, argv[0], private_mode_character); if (flag != NULL) *flag = false; - - /* Close alternate screen buffer */ - if (argv[0] == GUAC_TERMINAL_ALT_BUFFER) - guac_terminal_switch_buffers(term, false); break; @@ -1229,7 +1379,7 @@ int guac_terminal_open_pipe_stream(guac_terminal* term, unsigned char c) { length = 0; /* Parse parameter string as integer flags */ - flags |= atoi(param); + flags |= guac_terminal_parse_numeric_param(param); } @@ -1272,7 +1422,7 @@ int guac_terminal_set_scrollback(guac_terminal* term, unsigned char c) { length = 0; /* Assign scrollback size */ - term->requested_scrollback = atoi(param); + term->requested_scrollback = guac_terminal_parse_numeric_param(param); /* Update scrollbar bounds */ guac_terminal_scrollbar_set_bounds(term->scrollbar, diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index ca63f85dd..d98e6117a 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -45,11 +45,13 @@ #include #include +#include #include #include #include #include #include +#include /** * Sets the given range of columns to the given character. @@ -60,7 +62,7 @@ static void __guac_terminal_set_columns(guac_terminal* terminal, int row, guac_terminal_display_set_columns(terminal->display, row + terminal->scroll_offset, start_column, end_column, character); - guac_terminal_buffer_set_columns(terminal->buffer, row, + guac_terminal_buffer_set_columns(terminal->current_buffer, row, start_column, end_column, character); /* Clear selection if region is modified */ @@ -68,89 +70,6 @@ static void __guac_terminal_set_columns(guac_terminal* terminal, int row, } -/** - * Enforces a character break at the given edge, ensuring that the left side - * of the edge is the final column of a character, and the right side of the - * edge is the initial column of a DIFFERENT character. - * - * For a character in a column N, the left edge number is N, and the right - * edge is N+1. - */ -static void __guac_terminal_force_break(guac_terminal* terminal, int row, int edge) { - - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - - /* Ensure character to left of edge is unbroken */ - if (edge > 0) { - - int end_column = edge - 1; - int start_column = end_column; - - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); - - /* Determine start column */ - while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { - start_char--; - start_column--; - } - - /* Advance to start of broken character if necessary */ - if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { - start_column += start_char->width; - start_char += start_char->width; - } - - /* Clear character if broken */ - if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { - - guac_terminal_char cleared_char; - cleared_char.value = ' '; - cleared_char.attributes = start_char->attributes; - cleared_char.width = 1; - - __guac_terminal_set_columns(terminal, row, start_column, end_column, &cleared_char); - - } - - } - - /* Ensure character to right of edge is unbroken */ - if (edge >= 0 && edge < buffer_row->length) { - - int start_column = edge; - int end_column = start_column; - - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); - guac_terminal_char* end_char = &(buffer_row->characters[end_column]); - - /* Determine end column */ - while (end_column+1 < buffer_row->length && (end_char+1)->value == GUAC_CHAR_CONTINUATION) { - end_char++; - end_column++; - } - - /* Advance to start of broken character if necessary */ - if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { - start_column += start_char->width; - start_char += start_char->width; - } - - /* Clear character if broken */ - if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { - - guac_terminal_char cleared_char; - cleared_char.value = ' '; - cleared_char.attributes = start_char->attributes; - cleared_char.width = 1; - - __guac_terminal_set_columns(terminal, row, start_column, end_column, &cleared_char); - - } - - } - -} - /** * Returns the number of rows available within the terminal buffer, taking * changes to the desired scrollback size into account. Regardless of the @@ -179,11 +98,7 @@ static int guac_terminal_effective_buffer_length(guac_terminal* term) { /* If the buffer contains more rows than requested, pretend it only * contains the requested number of rows */ - int effective_length = term->buffer->length; - if (effective_length > scrollback) - effective_length = scrollback; - - return effective_length; + return guac_terminal_buffer_effective_length(term->current_buffer, scrollback); } @@ -215,8 +130,7 @@ void guac_terminal_reset(guac_terminal* term) { term->cursor_visible = true; /* Clear scrollback, buffer, and scroll region */ - term->buffer->top = 0; - term->buffer->length = 0; + guac_terminal_buffer_reset(term->current_buffer); term->scroll_start = 0; term->scroll_end = term->term_height - 1; term->scroll_offset = 0; @@ -469,9 +383,7 @@ guac_terminal* guac_terminal_create(guac_client* client, term->font_size = options->font_size; /* Init modified flag and conditional */ - term->modified = 0; - pthread_cond_init(&(term->modified_cond), NULL); - pthread_mutex_init(&(term->modified_lock), NULL); + guac_flag_init(&term->modified); /* Maximum and requested scrollback are initially the same */ term->max_scrollback = options->max_scrollback; @@ -484,10 +396,8 @@ guac_terminal* guac_terminal_create(guac_client* client, initial_scrollback = GUAC_TERMINAL_MAX_ROWS; /* Init current and alternate buffer */ - term->buffer = guac_terminal_buffer_alloc(initial_scrollback, - &default_char); - term->buffer_alt = NULL; - term->buffer_switched = false; + term->current_buffer = term->normal_buffer = guac_terminal_buffer_alloc(initial_scrollback, &default_char); + term->alternate_buffer = guac_terminal_buffer_alloc(GUAC_TERMINAL_MAX_ROWS, &default_char); /* Init display */ term->display = guac_terminal_display_alloc(client, @@ -633,16 +543,15 @@ void guac_terminal_free(guac_terminal* term) { /* Close and flush any active typescript */ guac_terminal_typescript_free(term->typescript); + /* Free scrollbar */ + guac_terminal_scrollbar_free(term->scrollbar); + /* Free display */ guac_terminal_display_free(term->display); /* Free buffers */ - guac_terminal_buffer_free(term->buffer); - if (term->buffer_alt != NULL) - guac_terminal_buffer_free(term->buffer_alt); - - /* Free scrollbar */ - guac_terminal_scrollbar_free(term->scrollbar); + guac_terminal_buffer_free(term->normal_buffer); + guac_terminal_buffer_free(term->alternate_buffer); /* Free copies of font and color scheme information */ guac_mem_free_const(term->color_scheme); @@ -652,47 +561,11 @@ void guac_terminal_free(guac_terminal* term) { guac_common_clipboard_free(term->clipboard); /* Free the terminal itself */ + pthread_mutex_destroy(&term->lock); guac_mem_free(term); } -/** - * Populate the given timespec with the current time, plus the given offset. - * - * @param ts - * The timespec structure to populate. - * - * @param offset_sec - * The offset from the current time to use when populating the given - * timespec, in seconds. - * - * @param offset_usec - * The offset from the current time to use when populating the given - * timespec, in microseconds. - */ -static void guac_terminal_get_absolute_time(struct timespec* ts, - int offset_sec, int offset_usec) { - - /* Get timeval */ - struct timeval tv; - gettimeofday(&tv, NULL); - - /* Update with offset */ - tv.tv_sec += offset_sec; - tv.tv_usec += offset_usec; - - /* Wrap to next second if necessary */ - if (tv.tv_usec >= 1000000) { - tv.tv_sec++; - tv.tv_usec -= 1000000; - } - - /* Convert to timespec */ - ts->tv_sec = tv.tv_sec; - ts->tv_nsec = tv.tv_usec * 1000; - -} - /** * Waits for the terminal state to be modified, returning only when the * specified timeout has elapsed or a frame flush is desired. Note that the @@ -711,32 +584,15 @@ static void guac_terminal_get_absolute_time(struct timespec* ts, */ static int guac_terminal_wait(guac_terminal* terminal, int msec_timeout) { - int retval = 1; - - pthread_mutex_t* mod_lock = &(terminal->modified_lock); - pthread_cond_t* mod_cond = &(terminal->modified_cond); - - /* Split provided milliseconds into microseconds and whole seconds */ - int secs = msec_timeout / 1000; - int usecs = (msec_timeout % 1000) * 1000; - - /* Calculate absolute timestamp from provided relative timeout */ - struct timespec timeout; - guac_terminal_get_absolute_time(&timeout, secs, usecs); - - /* Test for terminal modification */ - pthread_mutex_lock(mod_lock); - if (terminal->modified) - goto wait_complete; - - /* If not yet modified, wait for modification condition to be signaled */ - retval = pthread_cond_timedwait(mod_cond, mod_lock, &timeout) != ETIMEDOUT; + int retval = guac_flag_timedwait_and_lock(&terminal->modified, + GUAC_TERMINAL_MODIFIED, msec_timeout); -wait_complete: + /* Rest terminal modified state */ + if (retval) { + guac_flag_clear(&terminal->modified, GUAC_TERMINAL_MODIFIED); + guac_flag_unlock(&terminal->modified); + } - /* Terminal is no longer modified */ - terminal->modified = 0; - pthread_mutex_unlock(mod_lock); return retval; } @@ -751,7 +607,7 @@ int guac_terminal_render_frame(guac_terminal* terminal) { wait_result = guac_terminal_wait(terminal, 1000); if (wait_result || !terminal->started) { - guac_timestamp frame_start = guac_timestamp_current(); + guac_timestamp frame_start = client->last_sent_timestamp; do { @@ -788,16 +644,8 @@ int guac_terminal_read_stdin(guac_terminal* terminal, char* c, int size) { void guac_terminal_notify(guac_terminal* terminal) { - pthread_mutex_t* mod_lock = &(terminal->modified_lock); - pthread_cond_t* mod_cond = &(terminal->modified_cond); - - pthread_mutex_lock(mod_lock); - /* Signal modification */ - terminal->modified = 1; - pthread_cond_signal(mod_cond); - - pthread_mutex_unlock(mod_lock); + guac_flag_set(&terminal->modified, GUAC_TERMINAL_MODIFIED); } @@ -905,37 +753,37 @@ int guac_terminal_set(guac_terminal* term, int row, int col, int codepoint) { void guac_terminal_commit_cursor(guac_terminal* term) { - guac_terminal_char* guac_char; - - guac_terminal_buffer_row* row; - /* If no change, done */ if (term->cursor_visible && term->visible_cursor_row == term->cursor_row && term->visible_cursor_col == term->cursor_col) return; /* Clear cursor if it was visible */ if (term->visible_cursor_row != -1 && term->visible_cursor_col != -1) { - /* Get old row with cursor */ - row = guac_terminal_buffer_get_row(term->buffer, term->visible_cursor_row, term->visible_cursor_col+1); - guac_char = &(row->characters[term->visible_cursor_col]); - guac_char->attributes.cursor = false; - guac_terminal_display_set_columns(term->display, term->visible_cursor_row + term->scroll_offset, - term->visible_cursor_col, term->visible_cursor_col, guac_char); + guac_terminal_buffer_set_cursor(term->current_buffer, term->visible_cursor_row, term->visible_cursor_col, false); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->visible_cursor_row); + if (term->visible_cursor_col < length) + guac_terminal_display_set_columns(term->display, term->visible_cursor_row + term->scroll_offset, + term->visible_cursor_col, term->visible_cursor_col, &characters[term->visible_cursor_col]); + } /* Set cursor if should be visible */ if (term->cursor_visible) { - /* Get new row with cursor */ - row = guac_terminal_buffer_get_row(term->buffer, term->cursor_row, term->cursor_col+1); - guac_char = &(row->characters[term->cursor_col]); - guac_char->attributes.cursor = true; - guac_terminal_display_set_columns(term->display, term->cursor_row + term->scroll_offset, - term->cursor_col, term->cursor_col, guac_char); + guac_terminal_buffer_set_cursor(term->current_buffer, term->cursor_row, term->cursor_col, true); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->cursor_row); + if (term->cursor_col < length) + guac_terminal_display_set_columns(term->display, term->cursor_row + term->scroll_offset, + term->cursor_col, term->cursor_col, &characters[term->cursor_col]); term->visible_cursor_row = term->cursor_row; term->visible_cursor_col = term->cursor_col; + } /* Otherwise set visible position to a sentinel value */ @@ -971,9 +819,15 @@ int guac_terminal_write(guac_terminal* term, const char* buffer, int length) { } -int guac_terminal_scroll_up(guac_terminal* term, +void guac_terminal_scroll_up(guac_terminal* term, int start_row, int end_row, int amount) { + if (amount <= 0) + return; + + if (amount >= end_row - start_row + 1) + amount = end_row - start_row + 1; + /* If scrolling entire display, update scroll offset */ if (start_row == 0 && end_row == term->term_height - 1) { @@ -981,13 +835,7 @@ int guac_terminal_scroll_up(guac_terminal* term, guac_terminal_display_copy_rows(term->display, start_row + amount, end_row, -amount); /* Advance by scroll amount */ - term->buffer->top += amount; - if (term->buffer->top >= term->buffer->available) - term->buffer->top -= term->buffer->available; - - term->buffer->length += amount; - if (term->buffer->length > term->buffer->available) - term->buffer->length = term->buffer->available; + guac_terminal_buffer_scroll_up(term->current_buffer, amount); /* Reset scrollbar bounds */ guac_terminal_scrollbar_set_bounds(term->scrollbar, @@ -1019,10 +867,9 @@ int guac_terminal_scroll_up(guac_terminal* term, * type for visible cursor row and breaks display. */ guac_terminal_display_flush(term->display); - return 0; } -int guac_terminal_scroll_down(guac_terminal* term, +void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount) { guac_terminal_copy_rows(term, start_row, end_row - amount, amount); @@ -1036,7 +883,6 @@ int guac_terminal_scroll_down(guac_terminal* term, * type for visible cursor row and breaks display. */ guac_terminal_display_flush(term->display); - return 0; } int guac_terminal_clear_columns(guac_terminal* term, @@ -1173,16 +1019,16 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, for (row=start_row; row<=end_row; row++) { /* Get row from scrollback */ - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); /* Clear row */ guac_terminal_display_set_columns(terminal->display, dest_row, 0, terminal->display->width, &(terminal->default_char)); /* Draw row */ - guac_terminal_char* current = buffer_row->characters; - for (column=0; columnlength; column++) { + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { /* Only draw if not blank */ if (guac_terminal_is_visible(terminal, current)) @@ -1236,16 +1082,16 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, for (row=start_row; row<=end_row; row++) { /* Get row from scrollback */ - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); /* Clear row */ guac_terminal_display_set_columns(terminal->display, dest_row, 0, terminal->display->width, &(terminal->default_char)); /* Draw row */ - guac_terminal_char* current = buffer_row->characters; - for (column=0; columnlength; column++) { + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { /* Only draw if not blank */ if (guac_terminal_is_visible(terminal, current)) @@ -1270,7 +1116,7 @@ void guac_terminal_copy_columns(guac_terminal* terminal, int row, guac_terminal_display_copy_columns(terminal->display, row + terminal->scroll_offset, start_column, end_column, offset); - guac_terminal_buffer_copy_columns(terminal->buffer, row, + guac_terminal_buffer_copy_columns(terminal->current_buffer, row, start_column, end_column, offset); /* Clear selection if region is modified */ @@ -1282,10 +1128,6 @@ void guac_terminal_copy_columns(guac_terminal* terminal, int row, terminal->visible_cursor_col <= end_column) terminal->visible_cursor_col += offset; - /* Force breaks around destination region */ - __guac_terminal_force_break(terminal, row, start_column + offset); - __guac_terminal_force_break(terminal, row, end_column + offset + 1); - } void guac_terminal_copy_rows(guac_terminal* terminal, @@ -1294,7 +1136,7 @@ void guac_terminal_copy_rows(guac_terminal* terminal, guac_terminal_display_copy_rows(terminal->display, start_row + terminal->scroll_offset, end_row + terminal->scroll_offset, offset); - guac_terminal_buffer_copy_rows(terminal->buffer, + guac_terminal_buffer_copy_rows(terminal->current_buffer, start_row, end_row, offset); /* Clear selection if region is modified */ @@ -1327,10 +1169,6 @@ void guac_terminal_set_columns(guac_terminal* terminal, int row, } - /* Force breaks around destination region */ - __guac_terminal_force_break(terminal, row, start_column); - __guac_terminal_force_break(terminal, row, end_column + 1); - } static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int start_col, int end_row, int end_col) { @@ -1340,18 +1178,18 @@ static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int /* Redraw region */ for (row=start_row; row<=end_row; row++) { - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(term->buffer, row - term->scroll_offset, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, row - term->scroll_offset); /* Clear row */ guac_terminal_display_set_columns(term->display, row, start_col, end_col, &(term->default_char)); /* Copy characters */ - for (col=start_col; col <= end_col && col < buffer_row->length; col++) { + for (col=start_col; col <= end_col && col < length; col++) { /* Only redraw if not blank */ - guac_terminal_char* c = &(buffer_row->characters[col]); + guac_terminal_char* c = &characters[col]; if (guac_terminal_is_visible(term, c)) guac_terminal_display_set_columns(term->display, row, col, col, c); @@ -1364,6 +1202,15 @@ static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int /** * Internal terminal resize routine. Accepts width/height in CHARACTERS * (not pixels like the public function). + * + * @param term + * The terminal being resized. + * + * @param width + * The new width of the terminal, in characters. + * + * @param height + * The new height of the terminal, in characters. */ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { @@ -1386,7 +1233,7 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { shift_amount, term->display->height - 1, -shift_amount); /* Update buffer top and cursor row based on shift */ - term->buffer->top += shift_amount; + guac_terminal_buffer_scroll_up(term->current_buffer, shift_amount); term->cursor_row -= shift_amount; if (term->visible_cursor_row != -1) term->visible_cursor_row -= shift_amount; @@ -1421,7 +1268,7 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { shift_amount = available_scroll; /* Update buffer top and cursor row based on shift */ - term->buffer->top -= shift_amount; + guac_terminal_buffer_scroll_down(term->current_buffer, shift_amount); term->cursor_row += shift_amount; if (term->visible_cursor_row != -1) term->visible_cursor_row += shift_amount; @@ -1809,28 +1656,6 @@ static bool guac_terminal_is_blank(int ascii_char) { return (ascii_char == '\0' || ascii_char == ' '); } -/** - * Get the char (int ASCII code) at a specific row/col of the display. - * - * @param terminal - * The terminal on which we want to read a character. - * - * @param row - * The row where to read the character. - * - * @param col - * The column where to read the character. - * - * @return - * The ASCII code of the character at the given row/col. - */ -static int guac_terminal_get_char(guac_terminal* terminal, int row, int col) { - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - guac_terminal_char* ascii_char = &(buffer_row->characters[col]); - - return ascii_char->value; -} - /** * Selection of a word during a double click event. * - Fetching the character under the mouse cursor. @@ -1853,36 +1678,46 @@ static int guac_terminal_get_char(guac_terminal* terminal, int row, int col) { */ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col) { + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + + if (col >= length) + return; + /* (char)10 behind cursor */ - int cursor_char = guac_terminal_get_char(terminal, row, col); + int current_char = characters[col].value; /* Position of the word behind cursor. * Default = col required to select a char if not a word and not blank. */ - int word_head = col; - int word_tail = col; - int flag; /* The function used to calculate the word borders */ bool (*is_part_of_word)(int) = NULL; /* If selection is on a word, get its borders */ - if (guac_terminal_is_part_of_word(cursor_char)) + if (guac_terminal_is_part_of_word(current_char)) is_part_of_word = guac_terminal_is_part_of_word; /* If selection is on a blank, get its borders */ - else if (guac_terminal_is_blank(cursor_char)) + else if (guac_terminal_is_blank(current_char)) is_part_of_word = guac_terminal_is_blank; + int word_head = col; + int word_tail = col; + if (is_part_of_word != NULL) { + /* Get word head*/ - do { - flag = guac_terminal_get_char(terminal, row, word_head-1); - } while (is_part_of_word(flag) && (word_head >= 0 && word_head <= terminal->display->width) && word_head--); + for (; word_head - 1 >= 0; word_head--) { + if (!is_part_of_word(characters[word_head - 1].value)) + break; + } /* Get word tail */ - do { - flag = guac_terminal_get_char(terminal, row, word_tail+1); - } while (is_part_of_word(flag) && (word_tail >= 0 && word_tail <= terminal->display->width) && word_tail++); + for (; word_tail + 1 < terminal->display->width && word_tail + 1 < length; word_tail++) { + if (!is_part_of_word(characters[word_tail + 1].value)) + break; + } + } /* Select and add to clipboard the "word" */ @@ -2397,68 +2232,3 @@ void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) { /* Remove the user from the terminal cursor */ guac_common_cursor_remove_user(terminal->cursor, user); } - -void guac_terminal_switch_buffers(guac_terminal* terminal, bool to_alt) { - - /* Already on requested buffer */ - if (terminal->buffer_switched == to_alt) - return; - - /* Allocate alternate buffer */ - if (terminal->buffer_alt == NULL) - terminal->buffer_alt = guac_terminal_buffer_alloc( - terminal->display->height, &terminal->default_char); - - /* Keep new buffer state */ - terminal->buffer_switched = to_alt; - - /* Inversion of buffers pointers to switch to alternate */ - guac_terminal_buffer* temp_buffer = terminal->buffer; - terminal->buffer = terminal->buffer_alt; - terminal->buffer_alt = temp_buffer; - - /* Switch to alternate buffer */ - if (to_alt) { - - /* Backup cursor position before switching alternate buffer */ - terminal->visible_cursor_col_alt = terminal->visible_cursor_col; - terminal->visible_cursor_row_alt = terminal->visible_cursor_row; - terminal->cursor_col_alt = terminal->cursor_col; - terminal->cursor_row_alt = terminal->cursor_row; - - /* Clear screen content and selection */ - guac_terminal_reset(terminal); - - } - - /* Switch to normal buffer */ - else { - - /* Restore cursor position before switching normal buffer */ - terminal->visible_cursor_col = terminal->visible_cursor_col_alt; - terminal->visible_cursor_row = terminal->visible_cursor_row_alt; - terminal->cursor_col = terminal->cursor_col_alt; - terminal->cursor_row = terminal->cursor_row_alt; - - /* Repaint and resize overall display */ - guac_terminal_repaint_default_layer(terminal, terminal->client->socket); - __guac_terminal_redraw_rect(terminal, 0, 0, - terminal->term_height - 1, - terminal->term_width - 1); - - /* Restore scrollbar state */ - guac_terminal_scrollbar_set_bounds(terminal->scrollbar, - -guac_terminal_get_available_scroll(terminal), 0); - - /* Clear selection */ - terminal->text_selected = false; - terminal->selection_committed = false; - guac_terminal_notify(terminal); - - /* Free alternate buffer when unused */ - guac_terminal_buffer_free(terminal->buffer_alt); - terminal->buffer_alt = NULL; - - } - -} diff --git a/src/terminal/terminal/buffer.h b/src/terminal/terminal/buffer.h index cebb8e88e..9f175ba14 100644 --- a/src/terminal/terminal/buffer.h +++ b/src/terminal/terminal/buffer.h @@ -17,93 +17,37 @@ * under the License. */ -#ifndef _GUAC_TERMINAL_BUFFER_H -#define _GUAC_TERMINAL_BUFFER_H +#ifndef GUAC_TERMINAL_BUFFER_H +#define GUAC_TERMINAL_BUFFER_H /** - * Data structures and functions related to the terminal buffer. + * Data structures and functions related to the terminal buffer. The terminal + * buffer represents both the scrollback region and the current active contents + * of the terminal. + * + * NOTE: By design, all functions defined within this header make no + * assumptions about the validity of received coordinates, offsets, and + * lengths. Depending on the function, invalid values will be clamped, ignored, + * or reported as invalid. * * @file buffer.h */ #include "types.h" -/** - * A single variable-length row of terminal data. - */ -typedef struct guac_terminal_buffer_row { - - /** - * Array of guac_terminal_char representing the contents of the row. - */ - guac_terminal_char* characters; - - /** - * The length of this row in characters. This is the number of initialized - * characters in the buffer, usually equal to the number of characters - * in the screen width at the time this row was created. - */ - int length; - - /** - * The number of elements in the characters array. After the length - * equals this value, the array must be resized. - */ - int available; - - /** - * True if the current row has been wrapped to avoid going off the screen. - * False otherwise. - */ - bool wrapped_row; - -} guac_terminal_buffer_row; - /** * A buffer containing a constant number of arbitrary-length rows. * New rows can be appended to the buffer, with the oldest row replaced with * the new row. */ -typedef struct guac_terminal_buffer { - - /** - * The character to assign to newly-allocated cells. - */ - guac_terminal_char default_character; - - /** - * Array of buffer rows. This array functions as a ring buffer. - * When a new row needs to be appended, the top reference is moved down - * and the old top row is replaced. - */ - guac_terminal_buffer_row* rows; - - /** - * The index of the first row in the buffer (the row which represents row 0 - * with respect to the terminal display). This is also the index of the row - * to replace when insufficient space remains in the buffer to add a new - * row. - */ - int top; - - /** - * The number of rows currently stored in the buffer. - */ - int length; - - /** - * The number of rows in the buffer. This is the total capacity - * of the buffer. - */ - int available; - -} guac_terminal_buffer; +typedef struct guac_terminal_buffer guac_terminal_buffer; /** * Allocates a new buffer having the given maximum number of rows. New character cells will * be initialized to the given character. */ -guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* default_character); +guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, + const guac_terminal_char* default_character); /** * Frees the given buffer. @@ -111,10 +55,15 @@ guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* d void guac_terminal_buffer_free(guac_terminal_buffer* buffer); /** - * Returns the row at the given location. The row returned is guaranteed to be at least the given - * width. + * Resets the state of the given buffer such that it effectively no longer + * contains any rows. Space for previous rows, including the data from those + * previous rows, may still be maintained internally to avoid needing to + * reallocate rows again later. + * + * @param buffer + * The buffer to reset. */ -guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row, int width); +void guac_terminal_buffer_reset(guac_terminal_buffer* buffer); /** * Copies the given range of columns to a new location, offset from @@ -130,6 +79,36 @@ void guac_terminal_buffer_copy_columns(guac_terminal_buffer* buffer, int row, void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, int start_row, int end_row, int offset); +/** + * Scrolls the contents of the given buffer up by the given number of rows. + * Here, "scrolling up" refers to moving the row contents upwards within the + * buffer (ie: decreasing the row index of each row), NOT to moving the + * viewport up. + * + * @param buffer + * The buffer to scroll. + * + * @param amount + * The number of rows to scroll upwards. Zero and negative values have no + * effect. + */ +void guac_terminal_buffer_scroll_up(guac_terminal_buffer* buffer, int amount); + +/** + * Scrolls the contents of the given buffer down by the given number of rows. + * Here, "scrolling down" refers to moving the row contents downwards within + * the buffer (ie: increasing the row index of each row), NOT to moving the + * viewport down. + * + * @param buffer + * The buffer to scroll. + * + * @param amount + * The number of rows to scroll downwards. Zero and negative values have no + * effect. + */ +void guac_terminal_buffer_scroll_down(guac_terminal_buffer* buffer, int amount); + /** * Sets the given range of columns within the given row to the given * character. @@ -137,5 +116,75 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, guac_terminal_char* character); +/** + * Get the char (int ASCII code) at a specific row/col of the display. + * + * @param terminal + * The terminal on which we want to read a character. + * + * @param row + * The row where to read the character. + * + * @param col + * The column where to read the character. + * + * @return + * The ASCII code of the character at the given row/col. + */ +unsigned int guac_terminal_buffer_get_columns(guac_terminal_buffer* buffer, + guac_terminal_char** characters, bool* is_wrapped, int row); + +/** + * Returns the number of rows actually available for rendering within the given + * buffer, taking the scrollback size into account. Regardless of the true + * buffer length, only the number of rows that should be made available will be + * returned. + * + * @param buffer + * The buffer whose effective length should be retrieved. + * + * @param scrollback + * The number of rows currently within the terminal's scrollback buffer. + * + * @return + * The number of rows effectively available within the buffer. + */ +unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback); + +/** + * Sets whether the given buffer row was automatically wrapped by the terminal. + * Rows that were not automatically wrapped are lines of text that were printed + * and included an explicit newline character. + * + * @param buffer + * The buffer associated with the row being modified. + * + * @param row + * The row whose wrapped vs. not-wrapped state is being set. + * + * @param wrapped + * Whether the row was automatically wrapped (as opposed to simply ending + * with a newline character). + */ +void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped); + +/** + * Sets whether the character at the given row and column contains the cursor. + * + * @param buffer + * The buffer associated with character to modify. + * + * @param row + * The row of the character to modify. + * + * @param column + * The column of the character to modify. + * + * @param is_cursor + * Whether the character contains the cursor. + */ +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor); + #endif diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index cf9eb14c8..19d7d8da3 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -28,6 +28,14 @@ #include "terminal.h" #include "typescript.h" +#include + +/** + * The bitwise flag set on the modified flag of guac_terminal when the terminal + * state has been modified such that it is appropriate to flush a new frame. + */ +#define GUAC_TERMINAL_MODIFIED 1 + /** * Handler for characters printed to the terminal. When a character is printed, * the current char handler for the terminal is called and given that @@ -84,25 +92,13 @@ struct guac_terminal { */ pthread_mutex_t lock; - /** - * The mutex associated with the modified condition and flag, locked - * whenever a thread is waiting on the modified condition, the modified - * condition is being signalled, or the modified flag is being changed. - */ - pthread_mutex_t modified_lock; - /** * Flag set whenever an operation has affected the terminal in a way that - * will require a frame flush. When this flag is set, the modified_cond - * condition will be signalled. The modified_lock will always be - * acquired before this flag is altered. - */ - int modified; - - /** - * Condition which is signalled when the modified flag has been set - */ - pthread_cond_t modified_cond; + * will require a frame flush. + * + * @see GUAC_TERMINAL_MODIFIED + */ + guac_flag modified; /** * Pipe which will be the source of user input. When a terminal code @@ -243,11 +239,6 @@ struct guac_terminal { */ int cursor_row; - /** - * Backup of cursor_row when using alternate buffer. - */ - int cursor_row_alt; - /** * The current column location of the cursor. Note that while most * terminal operations will clip the cursor location within the bounds @@ -257,11 +248,6 @@ struct guac_terminal { */ int cursor_col; - /** - * Backup of cursor_col when using alternate buffer. - */ - int cursor_col_alt; - /** * The desired visibility state of the cursor. */ @@ -273,22 +259,12 @@ struct guac_terminal { */ int visible_cursor_row; - /** - * Backup of visible_cursor_row when using alternate buffer. - */ - int visible_cursor_row_alt; - /** * The column of the rendered cursor. * Will be set to -1 if the cursor is not visible. */ int visible_cursor_col; - /** - * Backup of visible_cursor_col when using alternate buffer. - */ - int visible_cursor_col_alt; - /** * The row of the saved cursor (ESC 7). */ @@ -325,23 +301,31 @@ struct guac_terminal { guac_terminal_display* display; /** - * Current terminal display state. All characters present on the screen - * are within this buffer. This has nothing to do with the display, which - * facilitates transfer of a set of changes to the remote display. + * The default, "normal" buffer containing all characters that should be + * displayed within the terminal emulator while not using the alternate + * buffer. Unless switched to the alternate buffer, all terminal operations + * will involve this buffer. The buffer that is relevant to terminal + * operations is determined by the current value of current_buffer. */ - guac_terminal_buffer* buffer; + guac_terminal_buffer* normal_buffer; /** - * Alternate buffer. + * The non-default, "alternate" buffer containing all characters that should + * be displayed within the terminal emulator while not using the normal + * buffer. Unless switched to the normal buffer, all terminal operations + * will involve this buffer. The buffer that is relevant to terminal + * operations is determined by the current value of current_buffer. */ - guac_terminal_buffer* buffer_alt; + guac_terminal_buffer* alternate_buffer; /** - * Actual state of the buffer: - * - true if switched to alternate buffer. - * - false if normal buffer. + * Pointer to the buffer representing the current text contents of the + * terminal, including any scrollback. All characters present on the screen + * are within this buffer. The buffer pointed to by this pointer may change + * over the course of the terminal session if console codes switch between + * the normal and alternate buffers. */ - bool buffer_switched; + guac_terminal_buffer* current_buffer; /** * Automatically place a tabstop every N characters. If zero, then no @@ -568,13 +552,13 @@ int guac_terminal_clear_range(guac_terminal* term, /** * Scrolls the terminal's current scroll region up by one row. */ -int guac_terminal_scroll_up(guac_terminal* term, +void guac_terminal_scroll_up(guac_terminal* term, int start_row, int end_row, int amount); /** * Scrolls the terminal's current scroll region down by one row. */ -int guac_terminal_scroll_down(guac_terminal* term, +void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount); /** @@ -698,18 +682,4 @@ void guac_terminal_copy_rows(guac_terminal* terminal, */ void guac_terminal_flush(guac_terminal* terminal); -/** - * Swith betwen normal and alternate buffer. - * - * @param terminal - * Terminal on which we switch buffers. - * - * @param to_alt - * Direction of buffer inversion. - * True if normal to alternate buffer. - * False if alternate to normal buffer. - */ -void guac_terminal_switch_buffers(guac_terminal* terminal, bool to_alt); - #endif - diff --git a/src/terminal/terminal/types.h b/src/terminal/terminal/types.h index 3e1f07468..8415737aa 100644 --- a/src/terminal/terminal/types.h +++ b/src/terminal/terminal/types.h @@ -54,29 +54,29 @@ typedef struct guac_terminal_attributes { /** * Whether the character should be rendered bold. */ - bool bold; + bool bold : 1; /** * Whether the character should be rendered with half brightness (faint * or low intensity). */ - bool half_bright; + bool half_bright : 1; /** - * Whether the character should be rendered with reversed colors - * (background becomes foreground and vice-versa). + * Whether the associated character is highlighted by the cursor. */ - bool reverse; + bool cursor : 1; /** - * Whether the associated character is highlighted by the cursor. + * Whether the character should be rendered with reversed colors + * (background becomes foreground and vice-versa). */ - bool cursor; + bool reverse : 1; /** * Whether to render the character with underscore. */ - bool underscore; + bool underscore : 1; /** * The foreground color of this character.