From 104006c6e24ccde1c99bc8e39a0264cf33609017 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 3 Jun 2024 14:22:53 -0700 Subject: [PATCH 01/53] GUACAMOLE-377: Establish public guac_display API for efficient rendering via Guacamole protocol. --- configure.ac | 5 + src/libguac/Makefile.am | 108 ++-- src/libguac/display-builtin-cursors.c | 149 +++++ src/libguac/display-builtin-cursors.h | 93 +++ src/libguac/display-cursor.c | 96 +++ src/libguac/display-flush.c | 298 +++++++++ src/libguac/display-layer-list.c | 430 +++++++++++++ src/libguac/display-layer.c | 250 ++++++++ src/libguac/display-plan-combine.c | 322 ++++++++++ src/libguac/display-plan-rect.c | 235 ++++++++ src/libguac/display-plan-search.c | 428 +++++++++++++ src/libguac/display-plan.c | 275 +++++++++ src/libguac/display-plan.h | 385 ++++++++++++ src/libguac/display-priv.h | 704 ++++++++++++++++++++++ src/libguac/display-worker.c | 602 ++++++++++++++++++ src/libguac/display.c | 331 ++++++++++ src/libguac/fifo.c | 210 +++++++ src/libguac/flag.c | 167 +++++ src/libguac/guacamole/assert.h | 47 ++ src/libguac/guacamole/display-constants.h | 47 ++ src/libguac/guacamole/display-types.h | 91 +++ src/libguac/guacamole/display.h | 616 +++++++++++++++++++ src/libguac/guacamole/fifo-constants.h | 57 ++ src/libguac/guacamole/fifo-types.h | 53 ++ src/libguac/guacamole/fifo.h | 314 ++++++++++ src/libguac/guacamole/flag-types.h | 36 ++ src/libguac/guacamole/flag.h | 244 ++++++++ src/libguac/guacamole/rect-types.h | 33 + src/libguac/guacamole/rect.h | 271 +++++++++ src/libguac/rect.c | 178 ++++++ src/libguac/tests/Makefile.am | 7 + src/libguac/tests/fifo/fifo.c | 298 +++++++++ src/libguac/tests/flag/flag.c | 168 ++++++ src/libguac/tests/rect/align.c | 70 +++ src/libguac/tests/rect/constrain.c | 42 ++ src/libguac/tests/rect/extend.c | 41 ++ src/libguac/tests/rect/init.c | 38 ++ src/libguac/tests/rect/intersects.c | 85 +++ 38 files changed, 7784 insertions(+), 40 deletions(-) create mode 100644 src/libguac/display-builtin-cursors.c create mode 100644 src/libguac/display-builtin-cursors.h create mode 100644 src/libguac/display-cursor.c create mode 100644 src/libguac/display-flush.c create mode 100644 src/libguac/display-layer-list.c create mode 100644 src/libguac/display-layer.c create mode 100644 src/libguac/display-plan-combine.c create mode 100644 src/libguac/display-plan-rect.c create mode 100644 src/libguac/display-plan-search.c create mode 100644 src/libguac/display-plan.c create mode 100644 src/libguac/display-plan.h create mode 100644 src/libguac/display-priv.h create mode 100644 src/libguac/display-worker.c create mode 100644 src/libguac/display.c create mode 100644 src/libguac/fifo.c create mode 100644 src/libguac/flag.c create mode 100644 src/libguac/guacamole/assert.h create mode 100644 src/libguac/guacamole/display-constants.h create mode 100644 src/libguac/guacamole/display-types.h create mode 100644 src/libguac/guacamole/display.h create mode 100644 src/libguac/guacamole/fifo-constants.h create mode 100644 src/libguac/guacamole/fifo-types.h create mode 100644 src/libguac/guacamole/fifo.h create mode 100644 src/libguac/guacamole/flag-types.h create mode 100644 src/libguac/guacamole/flag.h create mode 100644 src/libguac/guacamole/rect-types.h create mode 100644 src/libguac/guacamole/rect.h create mode 100644 src/libguac/rect.c create mode 100644 src/libguac/tests/fifo/fifo.c create mode 100644 src/libguac/tests/flag/flag.c create mode 100644 src/libguac/tests/rect/align.c create mode 100644 src/libguac/tests/rect/constrain.c create mode 100644 src/libguac/tests/rect/extend.c create mode 100644 src/libguac/tests/rect/init.c create mode 100644 src/libguac/tests/rect/intersects.c 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/libguac/Makefile.am b/src/libguac/Makefile.am index 2386f9c31..6aedb4042 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,63 @@ 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-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/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..682da3b50 --- /dev/null +++ b/src/libguac/display-cursor.c @@ -0,0 +1,96 @@ +/* + * 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; + display->pending_dirty = 1; + + guac_rwlock_release_lock(&display->pending_frame.lock); +} + +void guac_display_set_cursor(guac_display* display, + guac_display_cursor_type cursor_type) { + + 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; + + } + + guac_display_layer* cursor_layer = guac_display_cursor(display); + guac_display_layer_resize(cursor_layer, cursor->width, cursor->height); + + 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; + } + + context->dirty = (guac_rect) { + .left = 0, + .top = 0, + .right = cursor->width, + .bottom = cursor->height + }; + + guac_display_set_cursor_hotspot(display, cursor->hotspot_x, cursor->hotspot_y); + + guac_display_layer_close_raw(cursor_layer, context); + +} diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c new file mode 100644 index 000000000..57777b415 --- /dev/null +++ b/src/libguac/display-flush.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 "display-plan.h" +#include "display-priv.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. + */ +static void PFW_LFW_guac_display_frame_complete(guac_display* display) { + + guac_client* client = display->client; + + display->last_frame.layers = display->pending_frame.layers; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Copy over pending frame contents if actually changed */ + 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 }; + + } + + /* 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; + + } + + /* 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; + + } + + /* 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; + + } + + /* 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 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_dirty = 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); + + } + +} + +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); + if (display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY || display->active_workers) { + goto finished_with_display_ops; + } + + 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); + if (plan == NULL) + goto finished_with_last_frame_lock; + GUAC_DISPLAY_PLAN_END_PHASE(display, "draft", 1, 5); + + 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. + */ + + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + PFW_LFW_guac_display_frame_complete(display); + GUAC_DISPLAY_PLAN_END_PHASE(display, "commit", 5, 5); + +finished_with_last_frame_lock: + guac_rwlock_release_lock(&display->last_frame.lock); + +finished_with_display_ops: + guac_fifo_unlock(&display->ops); + 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..698cf2c1b --- /dev/null +++ b/src/libguac/display-layer-list.c @@ -0,0 +1,430 @@ +/* + * 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 PFW_LFW_guac_display_layer_buffers_resize(guac_display_layer_state* last_frame, + guac_display_layer_state* pending_frame, int width, int height) { + + GUAC_ASSERT(last_frame->buffer_width == pending_frame->buffer_width); + GUAC_ASSERT(last_frame->buffer_height == pending_frame->buffer_height); + + /* 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 == last_frame->buffer_width + && height == last_frame->buffer_height) + return; + + /* The request to resize applies only to the pending frame, but space for + * the last frame must be maintained. If either requested dimension is + * smaller than the last frame dimensions, the relevant dimension of the + * last frame must be used instead. */ + + int new_buffer_width = last_frame->buffer_width; + if (width > new_buffer_width) + new_buffer_width = width; + + int new_buffer_height = last_frame->buffer_height; + if (height > new_buffer_height) + new_buffer_height = height; + + /* Determine details of shared buffer space sufficient for both the + * established last frame and the resized pending frame. Allocate new + * shared buffer space for last and pending frames, interleaving their + * rows. + * + * NOTE: We interleave the rows of the last and pending frames to promote + * locality of reference. The comparisons performed between last and + * pending frames to determine what has changed are faster when the rows + * are interleaved, as data relevant to those comparisons will tend to be + * present in the CPU cache. */ + + int new_last_frame_offset = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, new_buffer_width); + int new_common_stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, new_last_frame_offset * 2); + unsigned char* new_buffer_base = guac_mem_zalloc(new_buffer_height, new_common_stride); + unsigned char* new_pending_frame_buffer = new_buffer_base; + unsigned char* new_last_frame_buffer = new_buffer_base + new_last_frame_offset; + + /* Copy over data from old shared buffer, if that data exists and is + * relevant */ + + if (last_frame->buffer != NULL && pending_frame->buffer != NULL) { + + guac_imgcpy( + + /* Copy to newly-allocated pending frame buffer ... */ + new_pending_frame_buffer, new_common_stride, + new_buffer_width, new_buffer_height, + + /* ... from old pending frame buffer. */ + pending_frame->buffer, pending_frame->buffer_stride, + pending_frame->buffer_width, pending_frame->buffer_height, + + /* All pixels are 32-bit */ + GUAC_DISPLAY_LAYER_RAW_BPP); + + guac_imgcpy( + + /* Copy to newly-allocated last frame buffer ... */ + new_last_frame_buffer, new_common_stride, + last_frame->buffer_width, last_frame->buffer_height, + + /* ... from old last frame buffer. */ + last_frame->buffer, last_frame->buffer_stride, + last_frame->buffer_width, last_frame->buffer_height, + + /* All pixels are 32-bit */ + GUAC_DISPLAY_LAYER_RAW_BPP); + + } + + guac_mem_free(pending_frame->buffer); + last_frame->buffer = new_buffer_base + new_last_frame_offset; + pending_frame->buffer = new_buffer_base; + + last_frame->buffer_width = pending_frame->buffer_width = new_buffer_width; + last_frame->buffer_height = pending_frame->buffer_height = new_buffer_height; + last_frame->buffer_stride = pending_frame->buffer_stride = new_common_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; + + /* Allocate shared buffer space for last and pending frames, interleaving + * their rows */ + + PFW_LFW_guac_display_layer_buffers_resize(last_frame, 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. + * Update the list head accordingly. */ + else { + 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. + * Update the list head accordingly. */ + else { + 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: Freeing pending_frame.buffer inherently also frees + * last_frame.buffer because they are actually interleaved views of the + * same block) */ + guac_mem_free(display_layer->pending_frame.buffer); + guac_mem_free(display_layer->pending_frame_cells); + + guac_mem_free(display_layer); + +} + +void PFW_LFW_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; + + } + + PFW_LFW_guac_display_layer_buffers_resize(&layer->last_frame, &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..0bde123bf --- /dev/null +++ b/src/libguac/display-layer.c @@ -0,0 +1,250 @@ +/* + * 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/display.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" + +#include +#include +#include + +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; + display->pending_dirty = 1; + + 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; + display->pending_dirty = 1; + + 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; + display->pending_dirty = 1; + + 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; + display->pending_dirty = 1; + + 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; + display->pending_dirty = 1; + + 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; + display->pending_dirty = 1; + + 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); + guac_rwlock_acquire_write_lock(&display->last_frame.lock); + + PFW_LFW_guac_display_layer_resize(layer, width, height); + display->pending_dirty = 1; + + guac_rwlock_release_lock(&display->last_frame.lock); + 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 }, + .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; + + guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + display->pending_dirty = 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); + + guac_display_layer_cairo_context* context = &(layer->pending_frame_cairo_context); + + context->dirty = (guac_rect) { 0 }; + 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); + display->pending_dirty = 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..517a02dc5 --- /dev/null +++ b/src/libguac/display-plan-combine.c @@ -0,0 +1,322 @@ +/* + * 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 operation can be combined with others. Only + * operations of certain types can be combined, and only up to a certain size + * (to favor parallelism). + * + * @param op + * The operation to test. + * + * @return + * Non-zero if the operation can be combined with others, zero otherwise. + */ +static int guac_display_plan_is_combinable(const guac_display_plan_operation* op) { + switch (op->type) { + + case GUAC_DISPLAY_PLAN_OPERATION_IMG: + return guac_rect_width(&op->dest) < GUAC_DISPLAY_MAX_COMBINED_WIDTH + && guac_rect_height(&op->dest) < GUAC_DISPLAY_MAX_COMBINED_HEIGHT; + + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + return 1; + + default: + return 0; + + } +} + +/** + * 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) { + + /* Consider only operations that have combinable types (draw to a + * particular rectangle in the layer) */ + if (!guac_display_plan_is_combinable(op_a) + || !guac_display_plan_is_combinable(op_b)) + return 0; + + /* Operations can only be combined within the same layer */ + if (op_a->layer != op_b->layer) + return 0; + + /* 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 in the same direction */ + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + if (guac_display_plan_has_common_edge(op_a, op_b)) { + + int delta_xa = op_a->dest.left - op_a->src.rect.left; + int delta_ya = op_a->dest.top - op_a->src.rect.top; + int delta_xb = op_b->dest.left - op_b->src.rect.left; + int delta_yb = op_b->dest.top - op_b->src.rect.top; + + return delta_xa == delta_xb + && delta_ya == delta_yb; + + } + 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); + + /* Other combinations require more complex logic... (see below) */ + default: + break; + + } + } + + /* Simulate combination */ + guac_rect combined = op_a->dest; + guac_rect_extend(&combined, &op_b->dest); + + /* 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.rect, &op_b->src.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_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..bad1253a4 --- /dev/null +++ b/src/libguac/display-plan-rect.c @@ -0,0 +1,235 @@ +/* + * 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; + + if (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..fef119cbe --- /dev/null +++ b/src/libguac/display-plan-search.c @@ -0,0 +1,428 @@ +/* + * 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* 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 && op->layer == 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(layer->last_frame, src_rect); + const unsigned char* copy_to = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(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, layer->last_frame.buffer_stride, + copy_to, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, layer->pending_frame.buffer_stride)) { + op->type = GUAC_DISPLAY_PLAN_OPERATION_COPY; + op->src.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) { + + 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..85f6a19cb --- /dev/null +++ b/src/libguac/display-plan.c @@ -0,0 +1,275 @@ +/* + * 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/display.h" +#include "guacamole/fifo.h" +#include "guacamole/mem.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; + } + +} + +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) { + + /* 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 last_frame_bounds = { + .left = 0, + .top = 0, + .right = current->last_frame.width, + .bottom = current->last_frame.height + }; + + 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 surfaces */ + guac_rect_constrain(&dirty, &last_frame_bounds); + 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 y = dirty.top; y < dirty.bottom; y += GUAC_DISPLAY_CELL_SIZE) { + + int height = GUAC_DISPLAY_CELL_SIZE; + if (y + height > dirty.bottom) + height = dirty.bottom - 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 (y + y_off) that are in each applicable cell. + * We jump forward by one cell for each comparison. */ + + 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 x = dirty.left; x < dirty.right; x += GUAC_DISPLAY_CELL_SIZE) { + + int width = GUAC_DISPLAY_CELL_SIZE; + if (x + width > dirty.right) + width = dirty.right - x; + + /* Mark the relevant region of the cell as dirty if the + * current 64-pixel line has changed in any way */ + size_t length = guac_mem_ckd_mul_or_die(width, GUAC_DISPLAY_LAYER_RAW_BPP); + if (memcmp(current_buffer, current_flushed, length)) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, x, y + y_off, width); + 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_display_plan_operation* op = plan->ops; + + for (int i = 0; i < plan->length; i++) { + guac_fifo_enqueue(&display->ops, op++); + } + +} diff --git a/src/libguac/display-plan.h b/src/libguac/display-plan.h new file mode 100644 index 000000000..3e8445055 --- /dev/null +++ b/src/libguac/display-plan.h @@ -0,0 +1,385 @@ +/* + * 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 to allow when combining any pair of rendering operations + * into a single operation, in pixels. 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. + */ +#define GUAC_DISPLAY_MAX_COMBINED_WIDTH 512 + +/** + * The maximum height to allow when combining any pair of rendering operations + * into a single operation, in pixels. 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. + */ +#define GUAC_DISPLAY_MAX_COMBINED_HEIGHT 512 + +/** + * 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. + */ + 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; + +/** + * 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_rect 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..62558f617 --- /dev/null +++ b/src/libguac/display-priv.h @@ -0,0 +1,704 @@ +/* + * 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. + * + * 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 BOTH the pending + * frame lock and the last frame lock, the pending frame lock MUST be acquired + * first to maintain consistent lock order and avoid deadlock conditions. All + * functions within guac_display will follow this order. + */ + +/** + * 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) + +/** + * 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; + + /** + * 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; + + /* ---------------- 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 since the last + * frame. + * + * IMPORTANT: The pending_frame.lock MUST be acquired before modifying or + * reading this member. + */ + int pending_dirty; + + /* ---------------- 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; + + /** + * Lock which ensures instructions that make use of a layer's current path + * (such as "rect" and "cfill") do not get inadvertently interleaved. + * Interleaving of path instructions can result in those paths not matching + * the expectatiosn of subsequent instructions, causing graphical + * artifacts. + */ + pthread_mutex_t op_path_lock; + + /** + * 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; + +}; + +/** + * 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_LFW_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-worker.c b/src/libguac/display-worker.c new file mode 100644 index 000000000..7a578b728 --- /dev/null +++ b/src/libguac/display-worker.c @@ -0,0 +1,602 @@ +/* + * 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.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" + +#include +#include +#include +#include + +/** + * Sends the contents of the given dirty rectangle from the given layer using + * lossless PNG compression. The resulting instructions will be sent over the + * client-wide broadcast socket associated with the given layer. The graphical + * contents sent will be pulled 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 sent to connected users. + * + * @param dirty + * The region of the layer that should be sent. + */ +static void LFR_guac_display_layer_flush_to_png(guac_display_layer* display_layer, + guac_rect* dirty) { + + guac_display* display = display_layer->display; + guac_client* client = display->client; + guac_socket* socket = client->socket; + const guac_layer* layer = display_layer->layer; + + /* Get Cairo layer for specified 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 */ + 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); + + /* Clear destination rect first */ + pthread_mutex_lock(&display->op_path_lock); + guac_protocol_send_rect(socket, layer, + dirty->left, dirty->top, + guac_rect_width(dirty), guac_rect_height(dirty)); + guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, + 0x00, 0x00, 0x00, 0xFF); + pthread_mutex_unlock(&display->op_path_lock); + + } + + /* Send PNG for rect */ + guac_client_stream_png(client, socket, GUAC_COMP_OVER, + layer, dirty->left, dirty->top, rect); + + cairo_surface_destroy(rect); + +} + +/** + * 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; + +} + +/** + * Sends the contents of the given dirty rectangle from the given layer using + * lossy JPEG compression. The resulting instructions will be sent over the + * client-wide broadcast socket associated with the given layer. The graphical + * contents sent will be pulled 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 layer + * The layer whose data should be sent to connected users. + * + * @param dirty + * The region of the layer that should be sent. + */ +static void LFR_guac_display_layer_flush_to_jpeg(guac_display_layer* display_layer, + guac_rect* dirty) { + + guac_display* display = display_layer->display; + guac_client* client = display->client; + guac_socket* socket = client->socket; + const guac_layer* layer = display_layer->layer; + + guac_rect max = { + .left = 0, + .top = 0, + .right = display_layer->last_frame.width, + .bottom = display_layer->last_frame.height + }; + + /* Expand the dirty rect size to fit in a grid with cells equal to the + * minimum JPEG block size */ + guac_rect_align(dirty, GUAC_SURFACE_JPEG_BLOCK_SIZE); + guac_rect_constrain(dirty, &max); + + /* Get Cairo layer for specified rect */ + unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); + + cairo_surface_t* 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); + + /* Send JPEG for rect */ + guac_client_stream_jpeg(client, socket, GUAC_COMP_OVER, layer, + dirty->left, dirty->top, rect, + guac_display_suggest_quality(client)); + + cairo_surface_destroy(rect); + +} + +/** + * Sends the contents of the given dirty rectangle from the given layer using + * WebP compression. Whether that WebP compression is lossless depends on the + * lossless setting of the layer's last frame. The resulting instructions will + * be sent over the client-wide broadcast socket associated with the given + * layer. The graphical contents sent will be pulled 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 layer + * The layer whose data should be sent to connected users. + * + * @param dirty + * The region of the layer that should be sent. + */ +static void LFR_guac_display_layer_flush_to_webp(guac_display_layer* display_layer, + guac_rect* dirty) { + + guac_display* display = display_layer->display; + guac_client* client = display->client; + guac_socket* socket = client->socket; + const guac_layer* layer = display_layer->layer; + + guac_rect max = { + .left = 0, + .top = 0, + .right = display_layer->last_frame.width, + .bottom = display_layer->last_frame.height + }; + + /* Expand the dirty rect size to fit in a grid with cells equal to the + * minimum WebP block size */ + guac_rect_align(dirty, GUAC_SURFACE_WEBP_BLOCK_SIZE); + guac_rect_constrain(dirty, &max); + + /* Get Cairo layer for specified 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 */ + 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); + + /* Send WebP for rect */ + 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); + + cairo_surface_destroy(rect); + +} + +/** + * 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_display_plan_operation op; + while (guac_fifo_dequeue_and_lock(&display->ops, &op)) { + + /* 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. */ + + /* Prefer WebP when reasonable */ + if (LFR_guac_display_layer_should_use_webp(display_layer, dirty, framerate)) + LFR_guac_display_layer_flush_to_webp(display_layer, dirty); + + /* 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)) + LFR_guac_display_layer_flush_to_jpeg(display_layer, dirty); + + /* Use PNG if no lossy formats are appropriate */ + else + LFR_guac_display_layer_flush_to_png(display_layer, dirty); + + break; + + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + guac_protocol_send_copy(client->socket, display_layer->last_frame_buffer, + op.src.rect.left, op.src.rect.top, guac_rect_width(&op.src.rect), guac_rect_height(&op.src.rect), + GUAC_COMP_OVER, display_layer->layer, op.dest.left, op.dest.top); + break; + + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + + pthread_mutex_lock(&display->op_path_lock); + 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); + + pthread_mutex_unlock(&display->op_path_lock); + break; + + case GUAC_DISPLAY_PLAN_OPERATION_NOP: + 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 { + + /* 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); + + /* Commit any changed contents to client-side backing + * buffer, while also determining whether any changes have + * been made to the mouse cursor graphic. */ + int cursor_modified = 0; + guac_display_layer* cursor = display->cursor_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)) { + + guac_protocol_send_copy(client->socket, current->layer, + 0, 0, current->last_frame.width, current->last_frame.height, + GUAC_COMP_SRC, current->last_frame_buffer, 0, 0); + + /* Additionally track whether the cursor is among + * the changed layers - we will later send a + * "cursor" instruction to actually update the + * remote cursor using the graphics from the cursor + * layer */ + if (current == cursor) + cursor_modified = 1; + + } + + current = current->last_frame.next; + + } + + /* Update the mouse cursor if it's been changed since the + * last frame */ + if (cursor_modified) { + 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); + } + + /* This is now absolutely everything for the current frame, + * and it's safe to flush any outstanding data */ + guac_socket_flush(client->socket); + + /* 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_rwlock_acquire_write_lock(&display->pending_frame.lock); + has_outstanding_frames = display->pending_dirty; + guac_rwlock_release_lock(&display->pending_frame.lock); + + } + + 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..ecda756d0 --- /dev/null +++ b/src/libguac/display.c @@ -0,0 +1,331 @@ +/* + * 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 + +/** + * 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 lock specific to the GUAC_DISPLAY_PLAN_OPERATION_RECT operation */ + pthread_mutex_init(&display->op_path_lock, NULL); + + 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; + 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); + + /* 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); + + pthread_mutex_destroy(&display->op_path_lock); + 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); + + 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); + + /* Get Cairo layer for specified rect */ + unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->last_frame, layer_bounds); + + cairo_surface_t* rect; + + /* Use RGB24 if the image is fully opaque */ + if (current->opaque) + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, width, height, + current->last_frame.buffer_stride); + + /* Otherwise ARGB32 is needed */ + else + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_ARGB32, width, height, + current->last_frame.buffer_stride); + + /* Send PNG for rect */ + guac_protocol_send_size(socket, layer, width, height); + 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_SRC, current->last_frame_buffer, 0, 0); + + /* 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); + + guac_protocol_send_sync(socket, client->last_sent_timestamp, display->last_frame.frames); + 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; + display->pending_dirty = 1; + guac_rwlock_release_lock(&display->pending_frame.lock); + + guac_display_end_multiple_frames(display, 0); + +} + +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/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/display-constants.h b/src/libguac/guacamole/display-constants.h new file mode 100644 index 000000000..56eba86ca --- /dev/null +++ b/src/libguac/guacamole/display-constants.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_DISPLAY_CONSTANTS_H +#define GUAC_DISPLAY_CONSTANTS_H + +/** + * 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..6014e9b89 --- /dev/null +++ b/src/libguac/guacamole/display-types.h @@ -0,0 +1,91 @@ +/* + * 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 + +/** + * 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 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..23eaafcfe --- /dev/null +++ b/src/libguac/guacamole/display.h @@ -0,0 +1,616 @@ +/* + * 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 + +/** + * Provides an abstract display implementation (guac_display), 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 + * + * @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 must be manually updated to + * cover any additional changed regions before closing the + * guac_display_layer_cairo_context. + */ + guac_rect dirty; + +}; + +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. + */ + 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. + */ + size_t stride; + + /** + * A rectangle covering the current bounds of the graphical surface. The + * buffer must not be addressed outside these bounds. + */ + guac_rect bounds; + + /** + * A rectangle covering the region of the guac_display_layer that has + * changed since the last frame. This rectangle must be manually updated to + * cover any additional changed regions before closing the + * guac_display_layer_raw_context. + */ + guac_rect dirty; + +}; + +/** + * 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_defaulta_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(). + * + * @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. + * + * @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, 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. + * + * @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. Changes to + * the remote mouse cursor will take effect after the current pending frame is + * complete. + * + * @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. + * + * @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. + * + * 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); + +#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/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/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); + +} + From 028fb58d17f8150073183d2f2a29610e30958b1b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 12 Jun 2024 16:09:58 -0700 Subject: [PATCH 02/53] GUACAMOLE-377: Tighten dirty rects down to the individual pixel, rather than whole 64-pixel lines. --- src/libguac/display-plan.c | 71 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c index 85f6a19cb..546284422 100644 --- a/src/libguac/display-plan.c +++ b/src/libguac/display-plan.c @@ -72,6 +72,71 @@ static void guac_display_plan_mark_dirty(guac_display_layer* layer, } +/** + * 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; @@ -162,9 +227,9 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { /* Mark the relevant region of the cell as dirty if the * current 64-pixel line has changed in any way */ - size_t length = guac_mem_ckd_mul_or_die(width, GUAC_DISPLAY_LAYER_RAW_BPP); - if (memcmp(current_buffer, current_flushed, length)) { - guac_display_plan_mark_dirty(current, current_cell, &op_count, x, y + y_off, width); + size_t length, pos; + if ((length = guac_display_memcmp(current_buffer, current_flushed, width, &pos)) != 0) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, x + pos, y + y_off, length); guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); } From 58140721eac30f47eb38b52cc33c52909ec104b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 12 Jun 2024 23:35:28 -0700 Subject: [PATCH 03/53] GUACAMOLE-377: Update layer list head upon layer removal only if non-empty. --- src/libguac/display-layer-list.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c index 698cf2c1b..c6f8ac5dc 100644 --- a/src/libguac/display-layer-list.c +++ b/src/libguac/display-layer-list.c @@ -339,9 +339,9 @@ void guac_display_remove_layer(guac_display_layer* display_layer) { 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. - * Update the list head accordingly. */ - else { + /* 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; } @@ -362,9 +362,9 @@ void guac_display_remove_layer(guac_display_layer* display_layer) { 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. - * Update the list head accordingly. */ - else { + /* 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; } From b965c4dd243f40771cb07406cb660403571a7580 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 13 Jun 2024 00:39:33 -0700 Subject: [PATCH 04/53] GUACAMOLE-377: Do NOT acquire the ops lock before any frame lock. --- src/libguac/display-flush.c | 12 +++++++----- src/libguac/display-priv.h | 13 ++++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index 57777b415..eb04c4445 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -224,10 +224,13 @@ void guac_display_end_multiple_frames(guac_display* display, int 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); - if (display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY || display->active_workers) { - goto finished_with_display_ops; - } + int encoding_in_progress = display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY || display->active_workers; + guac_fifo_unlock(&display->ops); + + if (encoding_in_progress) + goto finished_with_pending_frame_lock; guac_rwlock_acquire_write_lock(&display->last_frame.lock); @@ -286,8 +289,7 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) { finished_with_last_frame_lock: guac_rwlock_release_lock(&display->last_frame.lock); -finished_with_display_ops: - guac_fifo_unlock(&display->ops); +finished_with_pending_frame_lock: guac_rwlock_release_lock(&display->pending_frame.lock); if (plan != NULL) { diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 62558f617..6633118e3 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -73,9 +73,16 @@ /* * IMPORTANT: In cases where a single thread must acquire BOTH the pending - * frame lock and the last frame lock, the pending frame lock MUST be acquired - * first to maintain consistent lock order and avoid deadlock conditions. All - * functions within guac_display will follow this order. + * frame lock and the last frame lock, proper acquisition order must be + * observed to avoid deadlock. The correct order is: + * + * 1) pending_frame.lock + * 2) last_frame.lock + * + * The lock for the ops FIFO must NEVER be acquired before either of the frame + * locks. All operations involving the ops FIFO should be performed quickly + * and, if either pending_frame or last_frame must be involved, those locks + * must be acquired first. Doing otherwise risks deadlock. */ /** From 619eb2e57885fff5ec39ca06ffeff61bd0b13d85 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 13 Jun 2024 11:51:35 -0700 Subject: [PATCH 05/53] GUACAMOLE-377: Correct logic and lock order around deferred frame flushing (caused deadlock). --- src/libguac/display-cursor.c | 1 - src/libguac/display-flush.c | 6 +++--- src/libguac/display-layer.c | 11 ----------- src/libguac/display-priv.h | 32 ++++++++++++++++---------------- src/libguac/display-worker.c | 6 +++--- src/libguac/display.c | 1 - 6 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/libguac/display-cursor.c b/src/libguac/display-cursor.c index 682da3b50..11fb05a47 100644 --- a/src/libguac/display-cursor.c +++ b/src/libguac/display-cursor.c @@ -36,7 +36,6 @@ void guac_display_set_cursor_hotspot(guac_display* display, int x, int y) { display->pending_frame.cursor_hotspot_x = x; display->pending_frame.cursor_hotspot_y = y; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); } diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index eb04c4445..7ac7ece37 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -192,7 +192,6 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { display->last_frame.frames = display->pending_frame.frames; display->pending_frame.frames = 0; - display->pending_dirty = 0; /* Commit cursor hotspot */ display->last_frame.cursor_hotspot_x = display->pending_frame.cursor_hotspot_x; @@ -226,10 +225,11 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) { * the pending frame. */ guac_fifo_lock(&display->ops); - int encoding_in_progress = display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY || display->active_workers; + int defer_frame = display->frame_deferred = + (display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY) || display->active_workers; guac_fifo_unlock(&display->ops); - if (encoding_in_progress) + if (defer_frame) goto finished_with_pending_frame_lock; guac_rwlock_acquire_write_lock(&display->last_frame.lock); diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index 0bde123bf..12cd93588 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -49,7 +49,6 @@ void guac_display_layer_move(guac_display_layer* layer, int x, int y) { layer->pending_frame.x = x; layer->pending_frame.y = y; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -61,7 +60,6 @@ void guac_display_layer_stack(guac_display_layer* layer, int z) { guac_rwlock_acquire_write_lock(&display->pending_frame.lock); layer->pending_frame.z = z; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -73,7 +71,6 @@ void guac_display_layer_set_parent(guac_display_layer* layer, const guac_display guac_rwlock_acquire_write_lock(&display->pending_frame.lock); layer->pending_frame.parent = parent->layer; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -85,7 +82,6 @@ void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity) { guac_rwlock_acquire_write_lock(&display->pending_frame.lock); layer->pending_frame.opacity = opacity; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -97,7 +93,6 @@ void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless) { guac_rwlock_acquire_write_lock(&display->pending_frame.lock); layer->pending_frame.lossless = lossless; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -109,7 +104,6 @@ void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches) { guac_rwlock_acquire_write_lock(&display->pending_frame.lock); layer->pending_frame.touches = touches; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -122,7 +116,6 @@ void guac_display_layer_resize(guac_display_layer* layer, int width, int height) guac_rwlock_acquire_write_lock(&display->last_frame.lock); PFW_LFW_guac_display_layer_resize(layer, width, height); - display->pending_dirty = 1; guac_rwlock_release_lock(&display->last_frame.lock); guac_rwlock_release_lock(&display->pending_frame.lock); @@ -200,7 +193,6 @@ void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_ guac_display* display = layer->display; guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); @@ -241,10 +233,7 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay 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); - display->pending_dirty = 1; - guac_rwlock_release_lock(&display->pending_frame.lock); } diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 6633118e3..2ba94644c 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -72,17 +72,16 @@ */ /* - * IMPORTANT: In cases where a single thread must acquire BOTH the pending - * frame lock and the last frame lock, proper acquisition order must be - * observed to avoid deadlock. The correct order is: + * 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) op_path_lock * - * The lock for the ops FIFO must NEVER be acquired before either of the frame - * locks. All operations involving the ops FIFO should be performed quickly - * and, if either pending_frame or last_frame must be involved, those locks - * must be acquired first. Doing otherwise risks deadlock. + * Acquiring these locks in any other order risks deadlock. Don't do it. */ /** @@ -580,15 +579,6 @@ struct guac_display { */ guac_display_state pending_frame; - /** - * Whether the pending_frame has been modified in any way since the last - * frame. - * - * IMPORTANT: The pending_frame.lock MUST be acquired before modifying or - * reading this member. - */ - int pending_dirty; - /* ---------------- WELL-KNOWN LAYERS / BUFFERS ---------------- */ /** @@ -646,6 +636,16 @@ struct guac_display { */ 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; + }; /** diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index 7a578b728..076d0caff 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -572,9 +572,9 @@ void* guac_display_worker_thread(void* data) { guac_timestamp_msleep(required_wait); } - guac_rwlock_acquire_write_lock(&display->pending_frame.lock); - has_outstanding_frames = display->pending_dirty; - guac_rwlock_release_lock(&display->pending_frame.lock); + guac_fifo_lock(&display->ops); + has_outstanding_frames = display->frame_deferred; + guac_fifo_unlock(&display->ops); } diff --git a/src/libguac/display.c b/src/libguac/display.c index ecda756d0..51579dab8 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -287,7 +287,6 @@ void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user display->pending_frame.cursor_x = x; display->pending_frame.cursor_y = y; display->pending_frame.cursor_mask = mask; - display->pending_dirty = 1; guac_rwlock_release_lock(&display->pending_frame.lock); guac_display_end_multiple_frames(display, 0); From f898ead93fab38402bacc7f8e8188072d0df72b6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 13 Jun 2024 12:13:26 -0700 Subject: [PATCH 06/53] GUACAMOLE-377: Clarify logic around opaque layers. --- src/libguac/display-worker.c | 28 ++++++++-------------------- src/libguac/display.c | 19 ++++--------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index 076d0caff..3a627b62f 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -56,9 +56,8 @@ static void LFR_guac_display_layer_flush_to_png(guac_display_layer* display_laye guac_socket* socket = client->socket; const guac_layer* layer = display_layer->layer; - /* Get Cairo layer for specified rect */ + /* 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 */ @@ -67,7 +66,7 @@ static void LFR_guac_display_layer_flush_to_png(guac_display_layer* display_laye CAIRO_FORMAT_RGB24, guac_rect_width(dirty), guac_rect_height(dirty), display_layer->last_frame.buffer_stride); - /* Otherwise ARGB32 is needed */ + /* Otherwise ARGB32 is needed, and the destination must be cleared */ else { rect = cairo_image_surface_create_for_data(buffer, @@ -157,9 +156,8 @@ static void LFR_guac_display_layer_flush_to_jpeg(guac_display_layer* display_lay guac_rect_align(dirty, GUAC_SURFACE_JPEG_BLOCK_SIZE); guac_rect_constrain(dirty, &max); - /* Get Cairo layer for specified rect */ + /* Get Cairo surface covering dirty rect */ unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); - cairo_surface_t* 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); @@ -209,22 +207,12 @@ static void LFR_guac_display_layer_flush_to_webp(guac_display_layer* display_lay guac_rect_align(dirty, GUAC_SURFACE_WEBP_BLOCK_SIZE); guac_rect_constrain(dirty, &max); - /* Get Cairo layer for specified rect */ + /* 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 */ - 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); + cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, + display_layer->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + guac_rect_width(dirty), guac_rect_height(dirty), + display_layer->last_frame.buffer_stride); /* Send WebP for rect */ guac_client_stream_webp(client, socket, GUAC_COMP_OVER, layer, diff --git a/src/libguac/display.c b/src/libguac/display.c index 51579dab8..591a65490 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -197,22 +197,11 @@ void guac_display_dup(guac_display* display, guac_socket* socket) { int width = guac_rect_width(&layer_bounds); int height = guac_rect_height(&layer_bounds); - /* Get Cairo layer for specified rect */ + /* Get Cairo surface covering layer bounds */ unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->last_frame, layer_bounds); - - cairo_surface_t* rect; - - /* Use RGB24 if the image is fully opaque */ - if (current->opaque) - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_RGB24, width, height, - current->last_frame.buffer_stride); - - /* Otherwise ARGB32 is needed */ - else - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_ARGB32, width, height, - current->last_frame.buffer_stride); + 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_protocol_send_size(socket, layer, width, height); From 442b33eda4784f504e1a1e6c143ea3e49cbc986e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 18 Jun 2024 13:30:26 -0700 Subject: [PATCH 07/53] GUACAMOLE-377: Ensure regions outside last frame bounds are considered dirty. --- src/libguac/display-plan.c | 80 +++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c index 546284422..e8091250d 100644 --- a/src/libguac/display-plan.c +++ b/src/libguac/display-plan.c @@ -167,13 +167,6 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { * boundary of a cell */ guac_rect_align(&dirty, GUAC_DISPLAY_CELL_SIZE_EXPONENT); - guac_rect last_frame_bounds = { - .left = 0, - .top = 0, - .right = current->last_frame.width, - .bottom = current->last_frame.height - }; - guac_rect pending_frame_bounds = { .left = 0, .top = 0, @@ -181,8 +174,10 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { .bottom = current->pending_frame.height }; - /* Limit size of dirty rect by bounds of backing surfaces */ - guac_rect_constrain(&dirty, &last_frame_bounds); + /* 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); @@ -196,11 +191,11 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { * each cell to more accurately contain only what has actually changed * since last frame */ current->pending_frame.dirty = (guac_rect) { 0 }; - for (int y = dirty.top; y < dirty.bottom; y += GUAC_DISPLAY_CELL_SIZE) { + for (int corner_y = dirty.top; corner_y < dirty.bottom; corner_y += GUAC_DISPLAY_CELL_SIZE) { int height = GUAC_DISPLAY_CELL_SIZE; - if (y + height > dirty.bottom) - height = dirty.bottom - y; + 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 @@ -213,26 +208,65 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { /* At this point, we need to loop through the horizontal * dimension, comparing the 64-pixel rows of image data in the - * current line (y + y_off) that are in each applicable cell. - * We jump forward by one cell for each comparison. */ + * 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 x = dirty.left; x < dirty.right; x += GUAC_DISPLAY_CELL_SIZE) { + for (int corner_x = dirty.left; corner_x < dirty.right; corner_x += GUAC_DISPLAY_CELL_SIZE) { int width = GUAC_DISPLAY_CELL_SIZE; - if (x + width > dirty.right) - width = dirty.right - x; - - /* 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, width, &pos)) != 0) { - guac_display_plan_mark_dirty(current, current_cell, &op_count, x + pos, y + y_off, length); + 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++; From e396ed57fb243261594d46dd242e697fb3ee56ab Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 8 Jun 2024 13:41:46 -0700 Subject: [PATCH 08/53] GUACAMOLE-377: Migrate guac_mem_*() assertions to GUAC_ASSERT(). --- src/libguac/mem.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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; From 8afbeaece68cb43dde0ceac10c61c0fb28f8b8bd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 18 Jun 2024 15:46:15 -0700 Subject: [PATCH 09/53] GUACAMOLE-377: Do not render frames while users are joining. --- src/libguac/display-priv.h | 26 +++++++++++++++++++++++++- src/libguac/display-worker.c | 10 ++++++++++ src/libguac/display.c | 27 ++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 2ba94644c..18e41d041 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -79,7 +79,8 @@ * 1) pending_frame.lock * 2) last_frame.lock * 3) ops - * 4) op_path_lock + * 4) render_state + * 5) op_path_lock * * Acquiring these locks in any other order risks deadlock. Don't do it. */ @@ -193,6 +194,20 @@ #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 + /** * 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 @@ -646,6 +661,15 @@ struct guac_display { */ 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; + }; /** diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index 3a627b62f..32b9826b0 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -376,6 +376,11 @@ void* guac_display_worker_thread(void* data) { 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 */ @@ -535,6 +540,11 @@ void* guac_display_worker_thread(void* data) { * 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); diff --git a/src/libguac/display.c b/src/libguac/display.c index 591a65490..9893e3fe8 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -127,6 +127,11 @@ guac_display* guac_display_alloc(guac_client* client) { 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); + /* Init lock specific to the GUAC_DISPLAY_PLAN_OPERATION_RECT operation */ pthread_mutex_init(&display->op_path_lock, NULL); @@ -165,6 +170,13 @@ void guac_display_free(guac_display* display) { 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 */ + pthread_mutex_destroy(&display->op_path_lock); + 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) @@ -175,7 +187,6 @@ void guac_display_free(guac_display* display) { while (display->last_frame.layers != NULL) guac_display_free_layer(display->last_frame.layers); - pthread_mutex_destroy(&display->op_path_lock); guac_mem_free(display->worker_threads); guac_mem_free(display); @@ -186,6 +197,16 @@ 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) { @@ -252,7 +273,11 @@ void guac_display_dup(guac_display* display, guac_socket* socket) { 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); From 692400c0672c4a6dfbec57299335004b144e3597 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 18 Jun 2024 16:41:54 -0700 Subject: [PATCH 10/53] GUACAMOLE-377: Flush an automatic frame for mouse changes only if there are no other graphical changes pending. --- src/libguac/display-cursor.c | 13 ++++++++++-- src/libguac/display-flush.c | 12 +++++++++++ src/libguac/display-layer.c | 29 +++++++++++++++++++++++++++ src/libguac/display-priv.h | 13 ++++++++++++ src/libguac/display.c | 2 +- src/libguac/guacamole/display.h | 35 +++++++++++++++++++++++++++++---- 6 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/libguac/display-cursor.c b/src/libguac/display-cursor.c index 11fb05a47..6d119caa9 100644 --- a/src/libguac/display-cursor.c +++ b/src/libguac/display-cursor.c @@ -43,6 +43,7 @@ void guac_display_set_cursor_hotspot(guac_display* display, int x, int y) { 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) { @@ -65,9 +66,12 @@ void guac_display_set_cursor(guac_display* display, } + /* 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); @@ -81,6 +85,11 @@ void guac_display_set_cursor(guac_display* display, 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, @@ -88,8 +97,8 @@ void guac_display_set_cursor(guac_display* display, .bottom = cursor->height }; - guac_display_set_cursor_hotspot(display, cursor->hotspot_x, cursor->hotspot_y); - 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 index 7ac7ece37..4003c1478 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -192,6 +192,7 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { 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; @@ -213,6 +214,17 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { } +void guac_display_end_mouse_frame(guac_display* display) { + + guac_rwlock_acquire_read_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; diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index 12cd93588..c7ddae9de 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -26,6 +26,24 @@ #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; @@ -49,6 +67,7 @@ void guac_display_layer_move(guac_display_layer* layer, int x, int y) { layer->pending_frame.x = x; layer->pending_frame.y = y; + PFW_guac_display_layer_touch(layer); guac_rwlock_release_lock(&display->pending_frame.lock); @@ -60,6 +79,7 @@ void guac_display_layer_stack(guac_display_layer* layer, int z) { 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); @@ -71,6 +91,7 @@ void guac_display_layer_set_parent(guac_display_layer* layer, const guac_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); @@ -82,6 +103,7 @@ void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity) { 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); @@ -93,6 +115,7 @@ void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless) { 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); @@ -104,6 +127,7 @@ void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches) { 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); @@ -116,6 +140,7 @@ void guac_display_layer_resize(guac_display_layer* layer, int width, int height) guac_rwlock_acquire_write_lock(&display->last_frame.lock); PFW_LFW_guac_display_layer_resize(layer, width, height); + PFW_guac_display_layer_touch(layer); guac_rwlock_release_lock(&display->last_frame.lock); guac_rwlock_release_lock(&display->pending_frame.lock); @@ -193,6 +218,7 @@ void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_ guac_display* display = layer->display; guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + PFW_guac_display_layer_touch(layer); guac_rwlock_release_lock(&display->pending_frame.lock); @@ -233,7 +259,10 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay 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); + guac_rwlock_release_lock(&display->pending_frame.lock); } diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 18e41d041..67e24d3a2 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -594,6 +594,19 @@ struct guac_display { */ 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 ---------------- */ /** diff --git a/src/libguac/display.c b/src/libguac/display.c index 9893e3fe8..d1819a854 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -303,7 +303,7 @@ void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user display->pending_frame.cursor_mask = mask; guac_rwlock_release_lock(&display->pending_frame.lock); - guac_display_end_multiple_frames(display, 0); + guac_display_end_mouse_frame(display); } diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index 23eaafcfe..5ce7568d2 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -189,7 +189,8 @@ 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. + * mouse button. This function automatically invokes + * guac_display_end_mouse_frame(). * * @param display * The guac_display to notify. @@ -229,6 +230,18 @@ void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user */ 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 @@ -314,6 +327,11 @@ void guac_display_free_layer(guac_display_layer* display_layer); * 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. * @@ -324,9 +342,13 @@ void guac_display_free_layer(guac_display_layer* display_layer); guac_display_layer* guac_display_cursor(guac_display* display); /** - * Sets the remote mouse cursor to the given built-in cursor icon. Changes to - * the remote mouse cursor will take effect after the current pending frame is - * complete. + * 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. @@ -343,6 +365,11 @@ void guac_display_set_cursor(guac_display* display, * 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. * From 335d5e24a4e23700336f5e00417acd26892b41ab Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 19 Jun 2024 10:51:57 -0700 Subject: [PATCH 11/53] GUACAMOLE-377: Updates to cursor must occur within the boundary of their relevant frame (before "sync"). --- src/libguac/display-worker.c | 39 ++++++++++++------------------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index 32b9826b0..be66329f8 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -480,6 +480,18 @@ void* guac_display_worker_thread(void* data) { * 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 @@ -494,11 +506,7 @@ void* guac_display_worker_thread(void* data) { /* Allow connected clients to move forward with rendering */ guac_client_end_multiple_frames(client, display->last_frame.frames); - /* Commit any changed contents to client-side backing - * buffer, while also determining whether any changes have - * been made to the mouse cursor graphic. */ - int cursor_modified = 0; - guac_display_layer* cursor = display->cursor_buffer; + /* Commit any changed contents to client-side backing buffer */ guac_display_layer* current = display->last_frame.layers; while (current != NULL) { @@ -506,36 +514,15 @@ void* guac_display_worker_thread(void* data) { * been modified since the last frame */ guac_rect* dirty = ¤t->last_frame.dirty; if (!guac_rect_is_empty(dirty)) { - guac_protocol_send_copy(client->socket, current->layer, 0, 0, current->last_frame.width, current->last_frame.height, GUAC_COMP_SRC, current->last_frame_buffer, 0, 0); - - /* Additionally track whether the cursor is among - * the changed layers - we will later send a - * "cursor" instruction to actually update the - * remote cursor using the graphics from the cursor - * layer */ - if (current == cursor) - cursor_modified = 1; - } current = current->last_frame.next; } - /* Update the mouse cursor if it's been changed since the - * last frame */ - if (cursor_modified) { - 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); - } - /* This is now absolutely everything for the current frame, * and it's safe to flush any outstanding data */ guac_socket_flush(client->socket); From 15e400e0e1a6e0d3357809e8d4f5eefab42da286 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 19 Jun 2024 11:14:56 -0700 Subject: [PATCH 12/53] GUACAMOLE-377: Ensure all layers supporting transparency are properly cleared before receiving data that also supports transparency (including WebP). --- src/libguac/display-worker.c | 199 ++++++++++++----------------------- 1 file changed, 69 insertions(+), 130 deletions(-) diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index be66329f8..c570c6184 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -35,27 +35,28 @@ #include /** - * Sends the contents of the given dirty rectangle from the given layer using - * lossless PNG compression. The resulting instructions will be sent over the - * client-wide broadcast socket associated with the given layer. The graphical - * contents sent will be pulled 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. + * 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 sent to connected users. + * The layer whose data should be referenced by the returned Cairo surface. * * @param dirty - * The region of the layer that should be sent. + * 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 void LFR_guac_display_layer_flush_to_png(guac_display_layer* display_layer, +static cairo_surface_t* LFR_guac_display_layer_cairo_rect(guac_display_layer* display_layer, guac_rect* dirty) { - guac_display* display = display_layer->display; - guac_client* client = display->client; - guac_socket* socket = client->socket; - const guac_layer* layer = display_layer->layer; - /* Get Cairo surface covering dirty rect */ unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); cairo_surface_t* rect; @@ -67,29 +68,53 @@ static void LFR_guac_display_layer_flush_to_png(guac_display_layer* display_laye guac_rect_height(dirty), display_layer->last_frame.buffer_stride); /* Otherwise ARGB32 is needed, and the destination must be cleared */ - else { - + 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); - /* Clear destination rect first */ + 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) { + pthread_mutex_lock(&display->op_path_lock); - guac_protocol_send_rect(socket, layer, - dirty->left, dirty->top, + + guac_protocol_send_rect(socket, layer, dirty->left, dirty->top, guac_rect_width(dirty), guac_rect_height(dirty)); + guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, 0x00, 0x00, 0x00, 0xFF); + pthread_mutex_unlock(&display->op_path_lock); } - /* Send PNG for rect */ - guac_client_stream_png(client, socket, GUAC_COMP_OVER, - layer, dirty->left, dirty->top, rect); - - cairo_surface_destroy(rect); - } /** @@ -122,108 +147,6 @@ static int guac_display_suggest_quality(guac_client* client) { } -/** - * Sends the contents of the given dirty rectangle from the given layer using - * lossy JPEG compression. The resulting instructions will be sent over the - * client-wide broadcast socket associated with the given layer. The graphical - * contents sent will be pulled 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 layer - * The layer whose data should be sent to connected users. - * - * @param dirty - * The region of the layer that should be sent. - */ -static void LFR_guac_display_layer_flush_to_jpeg(guac_display_layer* display_layer, - guac_rect* dirty) { - - guac_display* display = display_layer->display; - guac_client* client = display->client; - guac_socket* socket = client->socket; - const guac_layer* layer = display_layer->layer; - - guac_rect max = { - .left = 0, - .top = 0, - .right = display_layer->last_frame.width, - .bottom = display_layer->last_frame.height - }; - - /* Expand the dirty rect size to fit in a grid with cells equal to the - * minimum JPEG block size */ - guac_rect_align(dirty, GUAC_SURFACE_JPEG_BLOCK_SIZE); - guac_rect_constrain(dirty, &max); - - /* Get Cairo surface covering dirty rect */ - unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); - cairo_surface_t* 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); - - /* Send JPEG for rect */ - guac_client_stream_jpeg(client, socket, GUAC_COMP_OVER, layer, - dirty->left, dirty->top, rect, - guac_display_suggest_quality(client)); - - cairo_surface_destroy(rect); - -} - -/** - * Sends the contents of the given dirty rectangle from the given layer using - * WebP compression. Whether that WebP compression is lossless depends on the - * lossless setting of the layer's last frame. The resulting instructions will - * be sent over the client-wide broadcast socket associated with the given - * layer. The graphical contents sent will be pulled 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 layer - * The layer whose data should be sent to connected users. - * - * @param dirty - * The region of the layer that should be sent. - */ -static void LFR_guac_display_layer_flush_to_webp(guac_display_layer* display_layer, - guac_rect* dirty) { - - guac_display* display = display_layer->display; - guac_client* client = display->client; - guac_socket* socket = client->socket; - const guac_layer* layer = display_layer->layer; - - guac_rect max = { - .left = 0, - .top = 0, - .right = display_layer->last_frame.width, - .bottom = display_layer->last_frame.height - }; - - /* Expand the dirty rect size to fit in a grid with cells equal to the - * minimum WebP block size */ - guac_rect_align(dirty, GUAC_SURFACE_WEBP_BLOCK_SIZE); - guac_rect_constrain(dirty, &max); - - /* Get Cairo surface covering dirty rect */ - unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); - cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, - display_layer->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, - guac_rect_width(dirty), guac_rect_height(dirty), - display_layer->last_frame.buffer_stride); - - /* Send WebP for rect */ - 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); - - cairo_surface_destroy(rect); - -} - /** * Guesses whether a rectangle within a particular layer would be better * compressed as PNG or using a lossy format like JPEG. Positive values @@ -372,6 +295,7 @@ void* guac_display_worker_thread(void* data) { 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)) { @@ -416,18 +340,33 @@ void* guac_display_worker_thread(void* data) { * 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)) - LFR_guac_display_layer_flush_to_webp(display_layer, dirty); + 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)) - LFR_guac_display_layer_flush_to_jpeg(display_layer, dirty); + 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 - LFR_guac_display_layer_flush_to_png(display_layer, dirty); + 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: From 4f3fccd798e92c3e432a30c82988a02e0f817ecc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 19 Jun 2024 12:14:12 -0700 Subject: [PATCH 13/53] GUACAMOLE-377: Add ability to "hint" that a drawing operation copied data from another layer. --- src/libguac/display-flush.c | 8 ++++++ src/libguac/display-layer.c | 10 ++++++++ src/libguac/display-plan-combine.c | 16 ++++++------ src/libguac/display-plan-search.c | 40 ++++++++++++++++++------------ src/libguac/display-plan.h | 22 +++++++++++++++- src/libguac/display-priv.h | 6 +++++ src/libguac/display-worker.c | 5 ++-- src/libguac/guacamole/display.h | 32 +++++++++++++++++++----- 8 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index 4003c1478..b53af93f2 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -177,6 +177,14 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { 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; diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index c7ddae9de..e95fc5cac 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -201,6 +201,7 @@ guac_display_layer_raw_context* guac_display_layer_open_raw(guac_display_layer* .buffer = layer->pending_frame.buffer, .stride = layer->pending_frame.buffer_stride, .dirty = { 0 }, + .hint_from = layer, .bounds = { .left = 0, .top = 0, @@ -220,6 +221,10 @@ void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_ 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); } @@ -232,6 +237,7 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay 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, @@ -263,6 +269,10 @@ void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_laye 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 index 517a02dc5..32542a1b4 100644 --- a/src/libguac/display-plan-combine.c +++ b/src/libguac/display-plan-combine.c @@ -125,14 +125,16 @@ static int guac_display_plan_should_combine(const guac_display_plan_operation* o switch (op_a->type) { /* Copy operations can be combined if they are perfectly adjacent - * (exactly share an edge) and copy in the same direction */ + * (exactly share an edge) and copy from the same source layer in + * the same direction */ case GUAC_DISPLAY_PLAN_OPERATION_COPY: - if (guac_display_plan_has_common_edge(op_a, op_b)) { + 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.rect.left; - int delta_ya = op_a->dest.top - op_a->src.rect.top; - int delta_xb = op_b->dest.left - op_b->src.rect.left; - int delta_yb = op_b->dest.top - op_b->src.rect.top; + 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; @@ -221,7 +223,7 @@ static int guac_display_plan_combine_if_improved(guac_display_plan_operation* op /* 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.rect, &op_b->src.rect); + guac_rect_extend(&op_a->src.layer_rect.rect, &op_b->src.layer_rect.rect); op_a->dirty_size += op_b->dirty_size; diff --git a/src/libguac/display-plan-search.c b/src/libguac/display-plan-search.c index fef119cbe..0d4c241be 100644 --- a/src/libguac/display-plan-search.c +++ b/src/libguac/display-plan-search.c @@ -374,13 +374,15 @@ static int guac_image_cmp(const unsigned char* restrict data_a, int width_a, int 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* layer = (guac_display_layer*) 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 && op->layer == layer) { + 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); @@ -388,14 +390,15 @@ static void PFR_LFR_guac_display_plan_find_copies(guac_display_plan* plan, 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(layer->last_frame, src_rect); - const unsigned char* copy_to = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(layer->pending_frame, dst_rect); + 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, layer->last_frame.buffer_stride, - copy_to, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, layer->pending_frame.buffer_stride)) { + 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.rect = src_rect; + op->src.layer_rect.layer = copy_from_layer->last_frame_buffer; + op->src.layer_rect.rect = src_rect; op->dest = dst_rect; } @@ -409,17 +412,22 @@ void PFR_LFR_guac_display_plan_rewrite_as_copies(guac_display_plan* plan) { guac_display_layer* current = display->last_frame.layers; while (current != NULL) { - guac_rect search_region; - guac_rect_init(&search_region, 0, 0, current->last_frame.width, current->last_frame.height); + /* 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); + /* 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); + 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.h b/src/libguac/display-plan.h index 3e8445055..76fecd0e2 100644 --- a/src/libguac/display-plan.h +++ b/src/libguac/display-plan.h @@ -134,6 +134,7 @@ typedef enum guac_display_plan_operation_type { /** * 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, @@ -155,6 +156,25 @@ typedef enum guac_display_plan_operation_type { } 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. */ @@ -208,7 +228,7 @@ typedef struct guac_display_plan_operation { * The rectangle that should be copied to the destination rect. This * value applies only to GUAC_DISPLAY_PLAN_OPERATION_COPY operations. */ - guac_rect rect; + guac_display_plan_layer_rect layer_rect; } src; diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 67e24d3a2..823dfc8e6 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -355,6 +355,12 @@ typedef struct guac_display_layer_state { */ guac_rect dirty; + /** + * Whether this layer should be searched for possible scroll/copy + * optimizations. + */ + int search_for_copies; + /* ---------------- LAYER LIST POINTERS ---------------- */ /** diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index c570c6184..c6b18bdf6 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -370,8 +370,9 @@ void* guac_display_worker_thread(void* data) { break; case GUAC_DISPLAY_PLAN_OPERATION_COPY: - guac_protocol_send_copy(client->socket, display_layer->last_frame_buffer, - op.src.rect.left, op.src.rect.top, guac_rect_width(&op.src.rect), guac_rect_height(&op.src.rect), + 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; diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index 5ce7568d2..9a63adb7f 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -88,12 +88,22 @@ struct guac_display_layer_cairo_context { /** * A rectangle covering the region of the guac_display_layer that has - * changed since the last frame. This rectangle must be manually updated to - * cover any additional changed regions before closing the - * guac_display_layer_cairo_context. + * 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 { @@ -125,12 +135,22 @@ struct guac_display_layer_raw_context { /** * A rectangle covering the region of the guac_display_layer that has - * changed since the last frame. This rectangle must be manually updated to - * cover any additional changed regions before closing the - * guac_display_layer_raw_context. + * 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; + }; /** From c994b2e92529e62c2b66186c50c0bb3ebe3d1563 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 6 Aug 2024 18:18:30 -0700 Subject: [PATCH 14/53] GUACAMOLE-377: Use 2 worker threads per available processor. --- src/libguac/display.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libguac/display.c b/src/libguac/display.c index d1819a854..2d627d4dd 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -41,6 +41,11 @@ #include #include +/** + * The number of worker threads to create per processor. + */ +#define GUAC_DISPLAY_CPU_THREAD_FACTOR 2 + /** * Returns the number of processors available to this process. If possible, * limits on otherwise available processors like CPU affinity will be taken @@ -146,7 +151,7 @@ guac_display* guac_display_alloc(guac_client* client) { "processor(s) are available.", cpu_count); } - display->worker_thread_count = 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); From 63ecfd81028a9a9c1329dc7461362c3969f9c500 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 6 Aug 2024 18:18:56 -0700 Subject: [PATCH 15/53] GUACAMOLE-377: Do not use threaded WebP encoder given that guac_display will already make use of a worker thread pool. --- src/libguac/encode-webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 */ From d6447224227689e39c4f447055e1ebeb91f29e7c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 3 Jun 2024 17:22:47 -0700 Subject: [PATCH 16/53] GUACAMOLE-377: Migrate VNC support to guac_display API. --- src/protocols/vnc/client.c | 5 +- src/protocols/vnc/client.h | 10 +-- src/protocols/vnc/cursor.c | 95 ++++++++++----------- src/protocols/vnc/cursor.h | 4 +- src/protocols/vnc/display.c | 162 ++++++++++++++++++------------------ src/protocols/vnc/display.h | 18 +++- src/protocols/vnc/input.c | 7 +- src/protocols/vnc/log.c | 1 - src/protocols/vnc/log.h | 1 - src/protocols/vnc/user.c | 8 +- src/protocols/vnc/vnc.c | 85 +++++-------------- src/protocols/vnc/vnc.h | 5 +- 12 files changed, 175 insertions(+), 226 deletions(-) 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..1120a4386 100644 --- a/src/protocols/vnc/client.h +++ b/src/protocols/vnc/client.h @@ -36,12 +36,12 @@ /** * 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. + * beginning a new frame, 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_START_TIMEOUT 1000000 +#define GUAC_VNC_FRAME_START_TIMEOUT 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..7a1bc1518 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,95 @@ #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); /* 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..076e00ca0 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,119 +49,103 @@ 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; - - /* Cairo image buffer */ - int stride; - unsigned char* buffer; - unsigned char* buffer_row_current; - cairo_surface_t* surface; + int rfb_height = client->height; + int rfb_width = client->width; - /* VNC framebuffer */ - unsigned int bpp; - unsigned int fb_stride; - unsigned char* fb_row_current; + /* 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_width, rfb_height); - /* Ignore extra update if already handled by copyrect */ - if (vnc_client->copy_rect_used) { - vnc_client->copy_rect_used = 0; - return; - } + /* Begin drawing operation directly to default layer */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); - /* Init Cairo buffer */ - stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w); - buffer = guac_mem_alloc(h, stride); - buffer_row_current = buffer; + /* Convert operation coordinates to guac_rect for easier manipulation */ + guac_rect op_bounds; + guac_rect_init(&op_bounds, x, y, w, h); - 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; dyformat.bitsPerPixel / 8; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, client->width); + const unsigned char* vnc_current_row = GUAC_RECT_CONST_BUFFER(op_bounds, client->frameBuffer, vnc_stride, vnc_bpp); - 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; + 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++) { - /* 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 = 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; + *(layer_current_pixel++) = 0xFF000000 | (blue << 16) | (green << 8) | red; else - *(buffer_current++) = (red << 16) | (green << 8) | blue; + *(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); + /* 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); + /* Draw operation is now complete */ + guac_display_layer_close_raw(default_layer, context); } -void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { +void guac_vnc_update_finished(rfbClient* client) { 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); + guac_display_end_multiple_frames(vnc_client->display, 1); + +} + +void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { + + guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); + guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; vnc_client->copy_rect_used = 1; @@ -305,17 +288,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)); @@ -380,7 +376,7 @@ rfbBool guac_vnc_malloc_framebuffer(rfbClient* rfb_client) { /* Resize surface */ if (vnc_client->display != NULL) - guac_common_surface_resize(vnc_client->display->default_surface, + guac_display_layer_resize(guac_display_default_layer(vnc_client->display), rfb_client->width, rfb_client->height); /* Use original, wrapped proc */ diff --git a/src/protocols/vnc/display.h b/src/protocols/vnc/display.h index a5b56096c..fd1bcd04d 100644 --- a/src/protocols/vnc/display.h +++ b/src/protocols/vnc/display.h @@ -22,6 +22,7 @@ #include "config.h" +#include #include #include @@ -50,6 +51,17 @@ */ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h); +/** + * Callback invoked by libVNCServer when all binary image data for the current + * frame has been received from the VNC server. The image data that frame will + * have been exposed via previous calls to guac_vnc_update(). + * + * @param client + * The VNC client associated with the VNC session in which the new image + * was received. + */ +void guac_vnc_update_finished(rfbClient* client); + /** * Callback invoked by libVNCServer when it receives a CopyRect message. * CopyRect specified a rectangle of source data within the display and a @@ -109,13 +121,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..7c813a94f 100644 --- a/src/protocols/vnc/user.c +++ b/src/protocols/vnc/user.c @@ -21,7 +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" @@ -35,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -128,10 +128,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..85f0cbf3c 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; + rfb_client->FinishedFrameBufferUpdate = guac_vnc_update_finished; rfb_client->GotCopyRect = guac_vnc_copyrect; #ifdef ENABLE_VNC_TLS_LOCKING @@ -514,12 +514,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 +532,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,75 +543,28 @@ 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_timestamp last_frame_end = guac_timestamp_current(); + guac_display_end_frame(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); - 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; - + int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_FRAME_START_TIMEOUT); + if (wait_result == 0) + continue; + + /* Handle any message received */ + if (!HandleRFBServerMessage(rfb_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) 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); - } /* Kill client and finish connection */ diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index 16fcb0e4a..89173a49e 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 @@ -100,7 +99,7 @@ typedef struct guac_vnc_client { /** * The current display state. */ - guac_common_display* display; + guac_display* display; /** * Internal clipboard. From b030ced3ea3d17050c8b43318f4a8775cd6aba28 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 6 Aug 2024 19:35:46 -0700 Subject: [PATCH 17/53] GUACAMOLE-377: Group all guac_display documentation together under a shared Doxygen topic. --- src/libguac/guacamole/display-constants.h | 9 +++++++++ src/libguac/guacamole/display-types.h | 9 +++++++++ src/libguac/guacamole/display.h | 18 +++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/libguac/guacamole/display-constants.h b/src/libguac/guacamole/display-constants.h index 56eba86ca..0f015a646 100644 --- a/src/libguac/guacamole/display-constants.h +++ b/src/libguac/guacamole/display-constants.h @@ -20,6 +20,11 @@ #ifndef GUAC_DISPLAY_CONSTANTS_H #define GUAC_DISPLAY_CONSTANTS_H +/** + * @addtogroup display + * @{ + */ + /** * Provides constants related to the abstract display implementation * (guac_display). @@ -44,4 +49,8 @@ */ #define GUAC_DISPLAY_LAYER_RAW_BPP 4 +/** + * @} + */ + #endif diff --git a/src/libguac/guacamole/display-types.h b/src/libguac/guacamole/display-types.h index 6014e9b89..3ca462956 100644 --- a/src/libguac/guacamole/display-types.h +++ b/src/libguac/guacamole/display-types.h @@ -20,6 +20,11 @@ #ifndef GUAC_DISPLAY_TYPES_H #define GUAC_DISPLAY_TYPES_H +/** + * @addtogroup display + * @{ + */ + /** * Provides type definitions related to the abstract display implementation * (guac_display). @@ -88,4 +93,8 @@ typedef enum guac_display_cursor_type { } guac_display_cursor_type; +/** + * @} + */ + #endif diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index 9a63adb7f..ea4e9f32f 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -21,8 +21,8 @@ #define GUAC_DISPLAY_H /** - * Provides an abstract display implementation (guac_display), which handles - * optimization automatically. Current optimizations include: + * An abstract display implementation which handles optimization automatically. + * Current optimizations include: * * - Scroll/copy detection * - Solid color detection @@ -31,6 +31,14 @@ * 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 */ @@ -440,7 +448,7 @@ void guac_display_layer_move(guac_display_layer* layer, int x, int y); * @param layer * The layer to set the stacking position of. * - * #param z + * @param z * The relative order of this layer. */ void guac_display_layer_stack(guac_display_layer* layer, int z); @@ -660,4 +668,8 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay */ void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_layer_cairo_context* context); +/** + * @} + */ + #endif From 3f88d3eb2464b53cd10eea2e6fed0fc012f60c9b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 17 Aug 2024 19:53:38 -0700 Subject: [PATCH 18/53] GUACAMOLE-377: Migrate RDP to guac_display. --- src/protocols/rdp/Makefile.am | 2 - src/protocols/rdp/bitmap.c | 85 +++---- src/protocols/rdp/bitmap.h | 5 +- src/protocols/rdp/channels/disp.c | 26 +- src/protocols/rdp/channels/disp.h | 16 ++ src/protocols/rdp/channels/rdpei.c | 4 +- src/protocols/rdp/client.c | 2 +- src/protocols/rdp/gdi.c | 382 ++--------------------------- src/protocols/rdp/gdi.h | 130 ---------- src/protocols/rdp/glyph.c | 170 ------------- src/protocols/rdp/glyph.h | 224 ----------------- src/protocols/rdp/input.c | 6 +- src/protocols/rdp/pointer.c | 76 +++--- src/protocols/rdp/pointer.h | 5 +- src/protocols/rdp/rdp.c | 41 +--- src/protocols/rdp/rdp.h | 7 +- src/protocols/rdp/user.c | 5 +- 17 files changed, 170 insertions(+), 1016 deletions(-) delete mode 100644 src/protocols/rdp/glyph.c delete mode 100644 src/protocols/rdp/glyph.h diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 57204f2aa..9b52f5a44 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -67,7 +67,6 @@ libguac_client_rdp_la_SOURCES = \ error.c \ fs.c \ gdi.c \ - glyph.c \ input.c \ keyboard.c \ keymap.c \ @@ -114,7 +113,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 index 6faa151fb..f13fae22d 100644 --- a/src/protocols/rdp/bitmap.c +++ b/src/protocols/rdp/bitmap.c @@ -18,14 +18,11 @@ */ #include "bitmap.h" -#include "common/display.h" -#include "common/surface.h" #include "config.h" #include "rdp.h" -#include #include -#include +#include #include #include @@ -38,22 +35,28 @@ void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap) { 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); + guac_display_layer* buffer = guac_display_alloc_buffer(rdp_client->display, 1); + guac_display_layer_resize(buffer, 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); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(buffer); - /* Send surface to buffer */ - guac_common_surface_draw(buffer->surface, 0, 0, image); + guac_rect dst_rect = { + .left = 0, + .top = 0, + .right = bitmap->width, + .bottom = bitmap->height + }; - /* Free surface */ - cairo_surface_destroy(image); + guac_rect_constrain(&dst_rect, &dst_context->bounds); + + guac_display_layer_raw_context_put(dst_context, &dst_rect, bitmap->data, 4 * bitmap->width); + + guac_rect_extend(&dst_context->dirty, &dst_rect); + + guac_display_layer_close_raw(buffer, dst_context); } @@ -79,54 +82,53 @@ 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; + guac_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; /* 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) { + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(default_layer); - /* 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); + guac_rect dst_rect = { + .left = bitmap->left, + .top = bitmap->top, + .right = bitmap->right, + .bottom = bitmap->bottom + }; - /* Draw image on default surface */ - guac_common_surface_draw(rdp_client->display->default_surface, - bitmap->left, bitmap->top, image); + guac_rect_constrain(&dst_rect, &dst_context->bounds); - /* Free surface */ - cairo_surface_destroy(image); + /* If cached, retrieve from cache */ + if (buffer != NULL) { + guac_display_layer_raw_context* src_context = guac_display_layer_open_raw(buffer); + guac_display_layer_raw_context_put(dst_context, &dst_rect, src_context->buffer, src_context->stride); + } + /* Otherwise, draw with stored image data */ + else if (bitmap->data != NULL) { + guac_display_layer_raw_context_put(dst_context, &dst_rect, bitmap->data, 4 * bitmap->width); } /* Increment usage counter */ ((guac_rdp_bitmap*) bitmap)->used++; + guac_rect_extend(&dst_context->dirty, &dst_rect); + dst_context->hint_from = buffer; + + guac_display_layer_close_raw(default_layer, dst_context); 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; + guac_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; /* If cached, free buffer */ if (buffer != NULL) - guac_common_display_free_buffer(rdp_client->display, buffer); + guac_display_free_layer(buffer); #ifndef FREERDP_BITMAP_FREE_FREES_BITMAP /* NOTE: Except in FreeRDP 2.0.0-rc0 and earlier, FreeRDP-allocated memory @@ -144,7 +146,7 @@ BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL pri guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; if (primary) - rdp_client->current_surface = rdp_client->display->default_surface; + rdp_client->current_surface = guac_display_default_layer(rdp_client->display); else { @@ -158,8 +160,7 @@ BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL pri if (((guac_rdp_bitmap*) bitmap)->layer == NULL) guac_rdp_cache_bitmap(context, bitmap); - rdp_client->current_surface = - ((guac_rdp_bitmap*) bitmap)->layer->surface; + rdp_client->current_surface = ((guac_rdp_bitmap*) bitmap)->layer; } diff --git a/src/protocols/rdp/bitmap.h b/src/protocols/rdp/bitmap.h index 297230c50..ab2529fe3 100644 --- a/src/protocols/rdp/bitmap.h +++ b/src/protocols/rdp/bitmap.h @@ -21,11 +21,10 @@ #define GUAC_RDP_BITMAP_H #include "config.h" -#include "common/display.h" #include #include -#include +#include #include /** @@ -41,7 +40,7 @@ typedef struct guac_rdp_bitmap { /** * Layer containing cached image data. */ - guac_common_display_layer* layer; + guac_display_layer* layer; /** * The number of times a bitmap has been used. 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/gdi.c b/src/protocols/rdp/gdi.c index 61bd87dd6..df8d44238 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -19,8 +19,7 @@ #include "bitmap.h" #include "color.h" -#include "common/display.h" -#include "common/surface.h" +#include "guacamole/display.h" #include "rdp.h" #include "settings.h" @@ -35,342 +34,6 @@ #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; @@ -395,8 +58,7 @@ void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) { /* 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_display_end_multiple_frames(rdp_client->display, rdp_client->frames_received); guac_socket_flush(client->socket); rdp_client->frames_received = 0; } @@ -446,11 +108,6 @@ 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; - /* Ignore paint if GDI output is suppressed */ if (gdi->suppressOutput) return TRUE; @@ -464,16 +121,20 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { 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_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(default_layer); - /* Send surface to buffer */ - guac_common_surface_draw(rdp_client->display->default_surface, x, y, surface); + guac_rect dst_rect; + guac_rect_init(&dst_rect, x, y, w, h); + guac_rect_constrain(&dst_rect, &dst_context->bounds); - /* Free surface */ - cairo_surface_destroy(surface); + guac_display_layer_raw_context_put(dst_context, &dst_rect, + GUAC_RECT_CONST_BUFFER(dst_rect, gdi->primary_buffer, gdi->stride, 4), + gdi->stride); + + guac_rect_extend(&dst_context->dirty, &dst_rect); + + guac_display_layer_close_raw(default_layer, dst_context); /* Next frame */ if (gdi->inGfxFrame) { @@ -489,17 +150,12 @@ 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; - guac_common_surface_resize(rdp_client->display->default_surface, - guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); - - guac_common_surface_reset_clip(rdp_client->display->default_surface); + int width = guac_rdp_get_width(context->instance); + int height = guac_rdp_get_height(context->instance); - 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)); + guac_display_layer_resize(guac_display_default_layer(rdp_client->display), width, height); + guac_client_log(client, GUAC_LOG_DEBUG, "Server resized display to %ix%i", width, height); - return gdi_resize(context->gdi, guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); + return gdi_resize(context->gdi, width, height); } diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h index 955bf7bd5..1f63a8fa2 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 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..e22e68d52 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; @@ -88,10 +83,33 @@ BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* poi if (rdp_client->frames_supported && !in_frame) guac_rdp_gdi_mark_frame(context, 1); + 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); + + 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); + + guac_display_layer_close_raw(cursor_layer, dst_context); + guac_display_layer_close_raw(src_layer, src_context); if (rdp_client->frames_supported && !in_frame) guac_rdp_gdi_mark_frame(context, 0); @@ -102,12 +120,10 @@ BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* poi 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 */ @@ -128,7 +144,7 @@ BOOL guac_rdp_pointer_set_null(rdpContext* context) { guac_rdp_gdi_mark_frame(context, 1); /* Set cursor to empty/blank graphic */ - guac_common_cursor_set_blank(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_NONE); if (rdp_client->frames_supported && !in_frame) guac_rdp_gdi_mark_frame(context, 0); @@ -151,7 +167,7 @@ BOOL guac_rdp_pointer_set_default(rdpContext* context) { guac_rdp_gdi_mark_frame(context, 1); /* Set cursor to embedded pointer */ - guac_common_cursor_set_pointer(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); if (rdp_client->frames_supported && !in_frame) guac_rdp_gdi_mark_frame(context, 0); 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..ff263d8ec 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -32,13 +32,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 +62,7 @@ #include #include #include +#include #include #include #include @@ -207,16 +206,6 @@ static BOOL rdp_freerdp_pre_connect(freerdp* instance) { 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 +226,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. @@ -524,15 +505,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 +555,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. @@ -686,8 +668,7 @@ static int guac_rdp_handle_connection(guac_client* client) { /* 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_display_end_multiple_frames(rdp_client->display, rdp_client->frames_received); guac_socket_flush(client->socket); rdp_client->frames_received = 0; } @@ -726,7 +707,7 @@ static int guac_rdp_handle_connection(guac_client* client) { rdp_client->keyboard = NULL; /* Free display */ - guac_common_display_free(rdp_client->display); + guac_display_free(rdp_client->display); rdp_client->display = NULL; guac_rwlock_release_lock(&(rdp_client->lock)); diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index c0e9ffacd..517e9e471 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,13 +98,13 @@ 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. 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) { From d99c5c0b3756063902184745b870d217640e5d64 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 17 Sep 2024 20:20:37 -0700 Subject: [PATCH 19/53] GUACAMOLE-377: Replace RDP settings string (rather than reuse storage of unknown size). --- src/protocols/rdp/settings.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index 4c62b9e2b..47320dfcc 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -1803,7 +1803,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); } From 28687cb8114ac023f6e71bdf7d1b3495c1ba6a64 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 31 Aug 2024 15:53:58 -0700 Subject: [PATCH 20/53] GUACAMOLE-377: Use FreeRDP's built-in GDI handling of bitmap cache. With FreeRDP's GDI being used for ther other aspects of RDP drawing, using our own implementation of bitmap caching causes the FreeRDP's GDI to become out-of-sync with Guacamole's representation, producing graphical artifacts. We can't simply monkey-patch the GDI, as the functions used are internal and not part of the public FreeRDP API. There are likely other possible approaches, like manually updating FreeRDP's GDI surface in addition to Guacamole's surface, but this may not be worth the effort given that bitmap caching is not commonly used by modern RDP servers. --- src/protocols/rdp/Makefile.am | 2 - src/protocols/rdp/bitmap.c | 169 ---------------------------------- src/protocols/rdp/bitmap.h | 132 -------------------------- src/protocols/rdp/gdi.c | 1 - src/protocols/rdp/rdp.c | 10 -- 5 files changed, 314 deletions(-) delete mode 100644 src/protocols/rdp/bitmap.c delete mode 100644 src/protocols/rdp/bitmap.h diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 9b52f5a44..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 \ @@ -86,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 \ diff --git a/src/protocols/rdp/bitmap.c b/src/protocols/rdp/bitmap.c deleted file mode 100644 index f13fae22d..000000000 --- a/src/protocols/rdp/bitmap.c +++ /dev/null @@ -1,169 +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 "config.h" -#include "rdp.h" - -#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_display_layer* buffer = guac_display_alloc_buffer(rdp_client->display, 1); - guac_display_layer_resize(buffer, bitmap->width, bitmap->height); - - /* Cache image data if present */ - if (bitmap->data != NULL) { - - guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(buffer); - - guac_rect dst_rect = { - .left = 0, - .top = 0, - .right = bitmap->width, - .bottom = bitmap->height - }; - - guac_rect_constrain(&dst_rect, &dst_context->bounds); - - guac_display_layer_raw_context_put(dst_context, &dst_rect, bitmap->data, 4 * bitmap->width); - - guac_rect_extend(&dst_context->dirty, &dst_rect); - - guac_display_layer_close_raw(buffer, dst_context); - - } - - /* 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_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - /* If not cached, cache if necessary */ - if (buffer == NULL && ((guac_rdp_bitmap*) bitmap)->used >= 1) - guac_rdp_cache_bitmap(context, bitmap); - - guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); - guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(default_layer); - - guac_rect dst_rect = { - .left = bitmap->left, - .top = bitmap->top, - .right = bitmap->right, - .bottom = bitmap->bottom - }; - - guac_rect_constrain(&dst_rect, &dst_context->bounds); - - /* If cached, retrieve from cache */ - if (buffer != NULL) { - guac_display_layer_raw_context* src_context = guac_display_layer_open_raw(buffer); - guac_display_layer_raw_context_put(dst_context, &dst_rect, src_context->buffer, src_context->stride); - } - - /* Otherwise, draw with stored image data */ - else if (bitmap->data != NULL) { - guac_display_layer_raw_context_put(dst_context, &dst_rect, bitmap->data, 4 * bitmap->width); - } - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - guac_rect_extend(&dst_context->dirty, &dst_rect); - dst_context->hint_from = buffer; - - guac_display_layer_close_raw(default_layer, dst_context); - return TRUE; - -} - -void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap) { - - guac_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - /* If cached, free buffer */ - if (buffer != NULL) - guac_display_free_layer(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 = guac_display_default_layer(rdp_client->display); - - 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; - - } - - return TRUE; - -} diff --git a/src/protocols/rdp/bitmap.h b/src/protocols/rdp/bitmap.h deleted file mode 100644 index ab2529fe3..000000000 --- a/src/protocols/rdp/bitmap.h +++ /dev/null @@ -1,132 +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 -#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_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/gdi.c b/src/protocols/rdp/gdi.c index df8d44238..3b1a973eb 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -17,7 +17,6 @@ * under the License. */ -#include "bitmap.h" #include "color.h" #include "guacamole/display.h" #include "rdp.h" diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index ff263d8ec..c19892608 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" @@ -197,15 +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 pointer handling */ rdpPointer pointer = *graphics->Pointer_Prototype; pointer.size = sizeof(guac_rdp_pointer); From 1f0a36513c981c775c1fe2c14b956b9b5e9657b7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 8 Aug 2024 14:09:11 -0700 Subject: [PATCH 21/53] GUACAMOLE-377: Migrate terminal emulator to guac_display. --- src/protocols/kubernetes/user.c | 1 - src/protocols/ssh/input.c | 2 - src/protocols/ssh/user.c | 1 - src/terminal/buffer.c | 438 ++++++++++-- src/terminal/common.c | 7 + src/terminal/display.c | 937 +++++++------------------- src/terminal/scrollbar.c | 186 +++-- src/terminal/select.c | 189 ++---- src/terminal/terminal-handlers.c | 196 +++++- src/terminal/terminal.c | 842 ++++------------------- src/terminal/terminal/buffer.h | 175 +++-- src/terminal/terminal/display.h | 297 ++++---- src/terminal/terminal/scrollbar.h | 45 +- src/terminal/terminal/select.h | 12 - src/terminal/terminal/terminal-priv.h | 140 ++-- src/terminal/terminal/terminal.h | 4 +- src/terminal/terminal/types.h | 13 +- 17 files changed, 1372 insertions(+), 2113 deletions(-) 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/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/terminal/buffer.c b/src/terminal/buffer.c index fe93fc4b6..ead57e613 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]); +} - /* If resizing is needed */ - if (width >= buffer_row->length) { +/** + * 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; + +} - /* 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); +/** + * 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; + + /* Limit maximum possible row size to the limits of the terminal display */ + if (length > GUAC_TERMINAL_MAX_COLUMNS) + length = GUAC_TERMINAL_MAX_COLUMNS; + + /* 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--; } - /* Initialize new part of row */ - first = &(buffer_row->characters[buffer_row->length]); - for (i=buffer_row->length; idefault_character; + /* 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; - 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,118 @@ 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); + +} + +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..5eaa4ce81 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -17,7 +17,8 @@ * under the License. */ -#include "common/surface.h" +#include "config.h" +#include "guacamole/rect.h" #include "terminal/common.h" #include "terminal/display.h" #include "terminal/palette.h" @@ -26,6 +27,7 @@ #include "terminal/types.h" #include +#include #include #include #include @@ -33,35 +35,126 @@ #include #include #include +#include #include #include #include #include -/* Maps any codepoint onto a number between 0 and 511 inclusive */ -int __guac_terminal_hash_codepoint(int codepoint) { +/** + * The palette index of the color to use when highlighting selected text. + */ +#define GUAC_TERMINAL_HIGHLIGHT_COLOR 4 + +/** + * Calculates the approximate luminance of the given color, where 0 represents + * no luminance and 255 represents full luminance. + * + * @param color + * The color to calculate the luminance of. + * + * @return + * The approximate luminance of the given color, on a scale of 0 through + * 255 inclusive. + */ +static int guac_terminal_color_luminance(const guac_terminal_color* color) { - /* If within one byte, just return codepoint */ - if (codepoint <= 0xFF) - return codepoint; + /* + * Y = 0.2126 R + 0.7152 G + 0.0722 B + * + * Here we multiply all coefficients by 16 to approximate luminance without + * having to resort to floating point, rounding to the nearest integer that + * minimizes error but still totals 16 when added to the other + * coefficients. + */ - /* Otherwise, map to next 256 values */ - return (codepoint & 0xFF) + 0x100; + return (3 * color->red + 12 * color->green + color->blue) / 16; } /** - * Sets the attributes of the display such that future glyphs will render as - * expected. + * Given the foreground and background colors of a character, updates those + * colors to represent the fact that the character has been highlighted + * (selected by the user). + * + * @param display + * The terminal display containing the character. + * + * @param glyph_foreground + * The foreground color of the character. The contents of this color may be + * modified to represent the effect of the highlight. + * + * @param glyph_background + * The background color of the character. The contents of this color may be + * modified to represent the effect of the highlight. */ -int __guac_terminal_set_colors(guac_terminal_display* display, - guac_terminal_attributes* attributes) { +static void guac_terminal_display_apply_highlight(guac_terminal_display* display, + guac_terminal_color* glyph_foreground, guac_terminal_color* glyph_background) { + + guac_terminal_color highlight; + guac_terminal_display_lookup_color(display, GUAC_TERMINAL_HIGHLIGHT_COLOR, &highlight); + + highlight.red = (highlight.red + glyph_background->red) / 2; + highlight.green = (highlight.green + glyph_background->green) / 2; + highlight.blue = (highlight.blue + glyph_background->blue) / 2; + + int foreground_lum = guac_terminal_color_luminance(glyph_foreground); + int background_lum = guac_terminal_color_luminance(glyph_background); + int highlight_lum = guac_terminal_color_luminance(&highlight); + + /* Replace background color for highlight color only if it's closer in + * perceived luminance to the backgrund color than it is to the + * foreground color (to preserve roughly the same degree of contrast) */ + if (abs(foreground_lum - highlight_lum) >= abs(background_lum - highlight_lum)) { + *glyph_background = highlight; + } + + /* If the highlight color can't be used while preserving contrast, + * simply inverting the colors will do the job */ + else { + guac_terminal_color temp = *glyph_background; + *glyph_background = *glyph_foreground; + *glyph_foreground = temp; + } + +} + +/** + * Given current attributes of a character, assigns foreground and background + * colors to represent that character state. + * + * @param display + * The terminal display containing the character. + * + * @param attributes + * All attributes associated with the character (bold, foreground color, + * background color, etc.). + * + * @param is_cursor + * Whether the terminal cursor is currently on top of the character. + * + * @param is_selected + * Whether the user currently has this character selected. + * + * @param glyph_foreground + * A pointer to the guac_terminal_color that should receive the foreground + * color of the character. + * + * @param glyph_background + * A pointer to the guac_terminal_color that should receive the background + * color of the character. + */ +static void guac_terminal_display_apply_render_attributes(guac_terminal_display* display, + guac_terminal_attributes* attributes, bool is_cursor, bool is_selected, + guac_terminal_color* glyph_foreground, + guac_terminal_color* glyph_background) { const guac_terminal_color* background; const guac_terminal_color* foreground; - /* Handle reverse video */ - if (attributes->reverse != attributes->cursor) { + /* Swap foreground and background color to represent reverse video and the + * cursor (this means that reverse and is_cursor cancel each other out) */ + if (is_cursor ? !attributes->reverse : attributes->reverse) { background = &attributes->foreground; foreground = &attributes->background; } @@ -70,7 +163,7 @@ int __guac_terminal_set_colors(guac_terminal_display* display, background = &attributes->background; } - /* Handle bold */ + /* Represent bold with the corresponding intense (brighter) color */ if (attributes->bold && !attributes->half_bright && foreground->palette_index >= GUAC_TERMINAL_FIRST_DARK && foreground->palette_index <= GUAC_TERMINAL_LAST_DARK) { @@ -78,89 +171,115 @@ int __guac_terminal_set_colors(guac_terminal_display* display, + GUAC_TERMINAL_INTENSE_OFFSET]; } - display->glyph_foreground = *foreground; + *glyph_foreground = *foreground; guac_terminal_display_lookup_color(display, - foreground->palette_index, &display->glyph_foreground); + foreground->palette_index, glyph_foreground); - display->glyph_background = *background; + *glyph_background = *background; guac_terminal_display_lookup_color(display, - background->palette_index, &display->glyph_background); + background->palette_index, glyph_background); /* Modify color if half-bright (low intensity) */ if (attributes->half_bright && !attributes->bold) { - display->glyph_foreground.red /= 2; - display->glyph_foreground.green /= 2; - display->glyph_foreground.blue /= 2; + glyph_foreground->red /= 2; + glyph_foreground->green /= 2; + glyph_foreground->blue /= 2; } - return 0; + /* Apply highlight if selected (NOTE: We re-swap foreground/background + * again here if the cursor is selected, as the sudden appearance of + * foreground color for an otherwise invisible character is surprising + * behavior) */ + if (is_selected) { + if (is_cursor) + guac_terminal_display_apply_highlight(display, glyph_background, glyph_foreground); + else + guac_terminal_display_apply_highlight(display, glyph_foreground, glyph_background); + } } /** - * Sends the given character to the terminal at the given row and column, - * rendering the character immediately. This bypasses the guac_terminal_display - * mechanism and is intended for flushing of updates only. + * Renders a single character at the given row and column. The character is + * rendered immediately to the underlying guac_display and will be sent to + * connected users when the next guac_display frame is completed. + * + * @param display + * The teriminal display receiving the character. + * + * @param row + * The row coordinate of the character, where 0 is the top-most row. While + * negative values generally represent rows of the scrollback buffer, + * supplying negative values here would result in rendering outside the + * visible display area and would be nonsensical. + * + * @param col + * The column coordinate of the character, where 0 is the left-most column. + * + * @param c + * The character to render. + * + * @param is_cursor + * Whether the terminal cursor is currently on top of the character. + * + * @param is_selected + * Whether the user currently has this character selected. */ -int __guac_terminal_set(guac_terminal_display* display, int row, int col, int codepoint) { +static void guac_terminal_display_render_glyph(guac_terminal_display* display, int row, int col, + guac_terminal_char* c, bool is_cursor, bool is_selected) { - int width; - - int bytes; - char utf8[4]; - - /* Use foreground color */ - const guac_terminal_color* color = &display->glyph_foreground; - - /* Use background color */ - const guac_terminal_color* background = &display->glyph_background; - - cairo_surface_t* surface; - cairo_t* cairo; - int surface_width, surface_height; - - PangoLayout* layout; - int layout_width, layout_height; - int ideal_layout_width, ideal_layout_height; + /* Use space if no glyph */ + int codepoint = c->value; + if (!guac_terminal_has_glyph(codepoint)) + codepoint = ' '; /* Calculate width in columns */ - width = wcwidth(codepoint); + int width = wcwidth(codepoint); if (width < 0) width = 1; /* Do nothing if glyph is empty */ if (width == 0) - return 0; + return; /* Convert to UTF-8 */ - bytes = guac_terminal_encode_utf8(codepoint, utf8); + char utf8[4]; + int bytes = guac_terminal_encode_utf8(codepoint, utf8); + + int glyph_x = display->char_width * col; + int glyph_y = display->char_height * row; + int glyph_width = width * display->char_width; + int glyph_height = display->char_height; - surface_width = width * display->char_width; - surface_height = display->char_height; + int ideal_layout_width = glyph_width * PANGO_SCALE; + int ideal_layout_height = glyph_height * PANGO_SCALE; - ideal_layout_width = surface_width * PANGO_SCALE; - ideal_layout_height = surface_height * PANGO_SCALE; + guac_display_layer_cairo_context* context = guac_display_layer_open_cairo(display->display_layer); + cairo_t* cairo = context->cairo; - /* Prepare surface */ - surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, - surface_width, surface_height); - cairo = cairo_create(surface); + cairo_identity_matrix(cairo); + cairo_translate(cairo, glyph_x, glyph_y); + + guac_terminal_color foreground, background; + guac_terminal_display_apply_render_attributes(display, &c->attributes, + is_cursor, is_selected, &foreground, &background); /* Fill background */ cairo_set_source_rgb(cairo, - background->red / 255.0, - background->green / 255.0, - background->blue / 255.0); + background.red / 255.0, + background.green / 255.0, + background.blue / 255.0); - cairo_rectangle(cairo, 0, 0, surface_width, surface_height); + cairo_rectangle(cairo, 0, 0, glyph_width, glyph_height); cairo_fill(cairo); /* Get layout */ - layout = pango_cairo_create_layout(cairo); + PangoLayout* layout = pango_cairo_create_layout(cairo); pango_layout_set_font_description(layout, display->font_desc); pango_layout_set_text(layout, utf8, bytes); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); + int layout_width, layout_height; pango_layout_get_size(layout, &layout_width, &layout_height); /* If layout bigger than available space, scale it back */ @@ -180,25 +299,21 @@ int __guac_terminal_set(guac_terminal_display* display, int row, int col, int co /* Draw */ cairo_set_source_rgb(cairo, - color->red / 255.0, - color->green / 255.0, - color->blue / 255.0); + foreground.red / 255.0, + foreground.green / 255.0, + foreground.blue / 255.0); cairo_move_to(cairo, 0.0, 0.0); pango_cairo_show_layout(cairo, layout); - /* Draw */ - guac_common_surface_draw(display->display_surface, - display->char_width * col, - display->char_height * row, - surface); - /* Free all */ g_object_unref(layout); - cairo_destroy(cairo); - cairo_surface_destroy(surface); - return 0; + guac_rect char_rect; + guac_rect_init(&char_rect, glyph_x, glyph_y, glyph_width, glyph_height); + guac_rect_extend(&context->dirty, &char_rect); + + guac_display_layer_close_cairo(display->display_layer, context); } @@ -216,6 +331,7 @@ static int get_margin_by_dpi(int dpi) { } guac_terminal_display* guac_terminal_display_alloc(guac_client* client, + guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]) { @@ -230,36 +346,29 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, display->char_height = 0; /* Create default surface */ - display->display_layer = guac_client_alloc_layer(client); - display->select_layer = guac_client_alloc_layer(client); - display->display_surface = guac_common_surface_alloc(client, - client->socket, display->display_layer, 0, 0); + display->graphical_display = graphical_display; + display->display_layer = guac_display_alloc_layer(display->graphical_display, 1); - /* Never use lossy compression for terminal contents */ - guac_common_surface_set_lossless(display->display_surface, 1); + /* Use blank (invisible) cursor by default */ + display->current_cursor = display->last_requested_cursor = GUAC_TERMINAL_CURSOR_BLANK; + guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); - /* Select layer is a child of the display layer */ - guac_protocol_send_move(client->socket, display->select_layer, - display->display_layer, 0, 0, 0); + /* Never use lossy compression for terminal contents */ + guac_display_layer_set_lossless(display->display_layer, 1); /* Calculate margin size by DPI */ display->margin = get_margin_by_dpi(dpi); /* Offset the Default Layer to make margins even on all sides */ - guac_protocol_send_move(client->socket, display->display_layer, - GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + guac_display_layer_move(display->display_layer, display->margin, display->margin); - display->default_foreground = display->glyph_foreground = *foreground; - display->default_background = display->glyph_background = *background; + display->default_foreground = *foreground; + display->default_background = *background; display->default_palette = palette; /* Initially empty */ display->width = 0; display->height = 0; - display->operations = NULL; - - /* Initially nothing selected */ - display->text_selected = false; /* Attempt to load font */ if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) { @@ -275,15 +384,15 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, void guac_terminal_display_free(guac_terminal_display* display) { + /* Free text rendering surface */ + guac_display_free_layer(display->display_layer); + /* Free font description */ pango_font_description_free(display->font_desc); /* Free default palette. */ guac_mem_free(display->default_palette); - /* Free operations buffers */ - guac_mem_free(display->operations); - /* Free display */ guac_mem_free(display); @@ -345,635 +454,105 @@ 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; - - src_current = &(display->operations[row * display->width + start_column]); - current = &(display->operations[row * display->width + start_column + offset]); - - /* Move data */ - memmove(current, src_current, - (end_column - start_column + 1) * sizeof(guac_terminal_operation)); - - /* Update operations */ - for (i=start_column; i<=end_column; i++) { - - /* If no operation here, set as copy */ - if (current->type == GUAC_CHAR_NOP) { - current->type = GUAC_CHAR_COPY; - current->row = row; - current->column = i; - } - - /* Next column */ - current++; - - } - -} - -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 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; - - src_current_row = &(display->operations[start_row * display->width]); - current_row = &(display->operations[(start_row + offset) * display->width]); - - /* Move data */ - memmove(current_row, src_current_row, - (end_row - start_row + 1) * sizeof(guac_terminal_operation) * display->width); - - /* Update operations */ - for (row=start_row; row<=end_row; row++) { - - guac_terminal_operation* current = current_row; - for (col=0; colwidth; col++) { - - /* If no operation here, set as copy */ - if (current->type == GUAC_CHAR_NOP) { - current->type = GUAC_CHAR_COPY; - current->row = row; - current->column = col; - } - - /* Next column */ - current++; - - } - - /* Next row */ - current_row += display->width; - - } - -} - -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; - - /* 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); - - current = &(display->operations[row * display->width + start_column]); - - /* For each column in range */ - for (i = start_column; i <= end_column; i += character->width) { - - /* Set operation */ - current->type = GUAC_CHAR_SET; - current->character = *character; - - /* Next character */ - current += character->width; - - } - -} - void guac_terminal_display_resize(guac_terminal_display* display, int width, int height) { /* Resize display only if dimensions have changed */ if (width == display->width && height == display->height) return; - guac_terminal_operation* current; - int x, y; - - /* Fill with background color */ - guac_terminal_char fill = { - .value = 0, - .attributes = { - .foreground = display->default_background, - .background = display->default_background - }, - .width = 1 - }; - - /* Free old operations buffer */ - if (display->operations != NULL) - guac_mem_free(display->operations); - - /* Alloc operations */ - display->operations = guac_mem_alloc(width, height, - sizeof(guac_terminal_operation)); - - /* Init each operation buffer row */ - current = display->operations; - for (y=0; ywidth && y < display->height) - current->type = GUAC_CHAR_NOP; - - /* Otherwise, clear contents first */ - else { - current->type = GUAC_CHAR_SET; - current->character = fill; - } - - current++; - - } - - } - /* Set width and height */ display->width = width; display->height = height; - /* Send display size */ - guac_common_surface_resize( - display->display_surface, - display->char_width * width, - display->char_height * height); - - guac_protocol_send_size(display->client->socket, - display->select_layer, - display->char_width * width, - display->char_height * height); - } -void __guac_terminal_display_flush_copy(guac_terminal_display* display) { - - guac_terminal_operation* current = display->operations; - int row, col; - - /* For each operation */ - for (row=0; rowheight; row++) { - for (col=0; colwidth; col++) { - - /* If operation is a copy operation */ - if (current->type == GUAC_CHAR_COPY) { - - /* The determined bounds of the rectangle of contiguous - * operations */ - int detected_right = -1; - int detected_bottom = row; - - /* The current row or column within a rectangle */ - int rect_row, rect_col; - - /* The dimensions of the rectangle as determined */ - int rect_width, rect_height; - - /* The expected row and column source for the next copy - * operation (if adjacent to current) */ - int expected_row, expected_col; - - /* Current row within a subrect */ - guac_terminal_operation* rect_current_row; - - /* Determine bounds of rectangle */ - rect_current_row = current; - expected_row = current->row; - for (rect_row=row; rect_rowheight; rect_row++) { - - guac_terminal_operation* rect_current = rect_current_row; - expected_col = current->column; - - /* Find width */ - for (rect_col=col; rect_colwidth; rect_col++) { - - /* If not identical operation, stop */ - if (rect_current->type != GUAC_CHAR_COPY - || rect_current->row != expected_row - || rect_current->column != expected_col) - break; - - /* Next column */ - rect_current++; - expected_col++; - - } - - /* If too small, cannot append row */ - if (rect_col-1 < detected_right) - break; - - /* As row has been accepted, update rect_row of rect */ - detected_bottom = rect_row; - - /* For now, only set rect_col bound if uninitialized */ - if (detected_right == -1) - detected_right = rect_col - 1; - - /* Next row */ - rect_current_row += display->width; - expected_row++; - - } - - /* Calculate dimensions */ - rect_width = detected_right - col + 1; - rect_height = detected_bottom - row + 1; - - /* Mark rect as NOP (as it has been handled) */ - rect_current_row = current; - expected_row = current->row; - for (rect_row=0; rect_rowcolumn; - - for (rect_col=0; rect_coltype == GUAC_CHAR_COPY - && rect_current->row == expected_row - && rect_current->column == expected_col) - rect_current->type = GUAC_CHAR_NOP; - - /* Next column */ - rect_current++; - expected_col++; - - } - - /* Next row */ - rect_current_row += display->width; - expected_row++; - - } - - /* Send copy */ - guac_common_surface_copy( - - display->display_surface, - current->column * display->char_width, - current->row * display->char_height, - rect_width * display->char_width, - rect_height * display->char_height, - - display->display_surface, - col * display->char_width, - row * display->char_height); - - } /* end if copy operation */ - - /* Next operation */ - current++; - - } - } - +void guac_terminal_display_set_cursor(guac_terminal_display* display, + guac_terminal_cursor_type cursor) { + display->last_requested_cursor = cursor; } -void __guac_terminal_display_flush_clear(guac_terminal_display* display) { - - guac_terminal_operation* current = display->operations; - int row, col; - - /* For each operation */ - for (row=0; rowheight; row++) { - for (col=0; colwidth; col++) { - - /* If operation is a clear operation (set to space) */ - if (current->type == GUAC_CHAR_SET && - !guac_terminal_has_glyph(current->character.value)) { - - /* The determined bounds of the rectangle of contiguous - * operations */ - int detected_right = -1; - int detected_bottom = row; - - /* The current row or column within a rectangle */ - int rect_row, rect_col; - - /* The dimensions of the rectangle as determined */ - int rect_width, rect_height; - - /* Color of the rectangle to draw */ - guac_terminal_color color; - if (current->character.attributes.reverse != current->character.attributes.cursor) - color = current->character.attributes.foreground; - else - color = current->character.attributes.background; - - /* Rely only on palette index if defined */ - guac_terminal_display_lookup_color(display, - color.palette_index, &color); - - /* Current row within a subrect */ - guac_terminal_operation* rect_current_row; - - /* Determine bounds of rectangle */ - rect_current_row = current; - for (rect_row=row; rect_rowheight; rect_row++) { - - guac_terminal_operation* rect_current = rect_current_row; - - /* Find width */ - for (rect_col=col; rect_colwidth; rect_col++) { - - const guac_terminal_color* joining_color; - if (rect_current->character.attributes.reverse != rect_current->character.attributes.cursor) - joining_color = &rect_current->character.attributes.foreground; - else - joining_color = &rect_current->character.attributes.background; - - /* If not identical operation, stop */ - if (rect_current->type != GUAC_CHAR_SET - || guac_terminal_has_glyph(rect_current->character.value) - || guac_terminal_colorcmp(joining_color, &color) != 0) - break; - - /* Next column */ - rect_current++; - - } - - /* If too small, cannot append row */ - if (rect_col-1 < detected_right) - break; - - /* As row has been accepted, update rect_row of rect */ - detected_bottom = rect_row; - - /* For now, only set rect_col bound if uninitialized */ - if (detected_right == -1) - detected_right = rect_col - 1; - - /* Next row */ - rect_current_row += display->width; - - } - - /* Calculate dimensions */ - rect_width = detected_right - col + 1; - rect_height = detected_bottom - row + 1; - - /* Mark rect as NOP (as it has been handled) */ - rect_current_row = current; - for (rect_row=0; rect_rowcharacter.attributes.reverse != rect_current->character.attributes.cursor) - joining_color = &rect_current->character.attributes.foreground; - else - joining_color = &rect_current->character.attributes.background; - - /* Mark clear operations as NOP */ - if (rect_current->type == GUAC_CHAR_SET - && !guac_terminal_has_glyph(rect_current->character.value) - && guac_terminal_colorcmp(joining_color, &color) == 0) - rect_current->type = GUAC_CHAR_NOP; +void guac_terminal_display_render_buffer(guac_terminal_display* display, + guac_terminal_buffer* buffer, int scroll_offset, + guac_terminal_char* default_char, + bool cursor_visible, int cursor_row, int cursor_col, + bool text_selected, int selection_start_row, int selection_start_col, + int selection_end_row, int selection_end_col) { - /* Next column */ - rect_current++; + if (selection_start_row > selection_end_row) { - } + int old_end_row = selection_end_row; + selection_end_row = selection_start_row; + selection_start_row = old_end_row; - /* Next row */ - rect_current_row += display->width; + int old_end_col = selection_end_col; + selection_end_col = selection_start_col; + selection_start_col = old_end_col; - } - - /* Send rect */ - guac_common_surface_set( - display->display_surface, - col * display->char_width, - row * display->char_height, - rect_width * display->char_width, - rect_height * display->char_height, - color.red, color.green, color.blue, - 0xFF); - - } /* end if clear operation */ - - /* Next operation */ - current++; - - } } - -} - -void __guac_terminal_display_flush_set(guac_terminal_display* display) { - - guac_terminal_operation* current = display->operations; - int row, col; - - /* For each operation */ - for (row=0; rowheight; row++) { - for (col=0; colwidth; col++) { - - /* Perform given operation */ - if (current->type == GUAC_CHAR_SET) { - - int codepoint = current->character.value; - - /* Use space if no glyph */ - if (!guac_terminal_has_glyph(codepoint)) - codepoint = ' '; - - /* Set attributes */ - __guac_terminal_set_colors(display, - &(current->character.attributes)); - - /* Send character */ - __guac_terminal_set(display, row, col, codepoint); - - /* Mark operation as handled */ - current->type = GUAC_CHAR_NOP; - - } - - /* Next operation */ - current++; - - } + else if (selection_start_row == selection_end_row && selection_start_col > selection_end_col) { + int old_end_col = selection_end_col; + selection_end_col = selection_start_col; + selection_start_col = old_end_col; } -} - -void guac_terminal_display_flush(guac_terminal_display* display) { + if (display->current_cursor != display->last_requested_cursor) { - /* Flush operations, copies first, then clears, then sets. */ - __guac_terminal_display_flush_copy(display); - __guac_terminal_display_flush_clear(display); - __guac_terminal_display_flush_set(display); + switch (display->last_requested_cursor) { - /* Flush surface */ - guac_common_surface_flush(display->display_surface); + case GUAC_TERMINAL_CURSOR_BLANK: + guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); + break; -} + case GUAC_TERMINAL_CURSOR_IBAR: + guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_IBAR); + break; -void guac_terminal_display_dup( - guac_terminal_display* display, guac_client* client, guac_socket* socket) { + case GUAC_TERMINAL_CURSOR_POINTER: + guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_POINTER); + break; - /* Create default surface */ - guac_common_surface_dup(display->display_surface, client, socket); + } - /* Select layer is a child of the display layer */ - guac_protocol_send_move(socket, display->select_layer, - display->display_layer, 0, 0, 0); + display->current_cursor = display->last_requested_cursor; - /* Offset the Default Layer to make margins even on all sides */ - guac_protocol_send_move(socket, display->display_layer, - GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + } - /* Send select layer size */ - guac_protocol_send_size(socket, display->select_layer, + guac_display_layer_resize(display->display_layer, display->char_width * display->width, display->char_height * display->height); -} - -void guac_terminal_display_select(guac_terminal_display* display, - int start_row, int start_col, int end_row, int end_col) { - - guac_socket* socket = display->client->socket; - guac_layer* select_layer = display->select_layer; + /* Redraw region */ + for (int row = 0; row < display->height; row++) { - /* Do nothing if selection is unchanged */ - if (display->text_selected - && display->selection_start_row == start_row - && display->selection_start_column == start_col - && display->selection_end_row == end_row - && display->selection_end_column == end_col) - return; - - /* Text is now selected */ - display->text_selected = true; - - display->selection_start_row = start_row; - display->selection_start_column = start_col; - display->selection_end_row = end_row; - display->selection_end_column = end_col; - - /* If single row, just need one rectangle */ - if (start_row == end_row) { + int adjusted_row = row - scroll_offset; - /* Ensure proper ordering of columns */ - if (start_col > end_col) { - int temp = start_col; - start_col = end_col; - end_col = temp; - } - - /* Select characters between columns */ - guac_protocol_send_rect(socket, select_layer, + guac_terminal_char* characters; + unsigned int length = guac_terminal_buffer_get_columns(buffer, &characters, NULL, adjusted_row); - start_col * display->char_width, - start_row * display->char_height, + /* Copy characters */ + for (int col = 0; col < display->width; col++) { - (end_col - start_col + 1) * display->char_width, - display->char_height); + bool is_cursor = cursor_visible + && adjusted_row == cursor_row + && col == cursor_col; - } + bool is_selected = text_selected + && adjusted_row >= selection_start_row + && adjusted_row <= selection_end_row + && (col >= selection_start_col || adjusted_row != selection_start_row) + && (col <= selection_end_col || adjusted_row != selection_end_row); - /* Otherwise, need three */ - else { - - /* Ensure proper ordering of start and end coords */ - if (start_row > end_row) { - - int temp; - - temp = start_row; - start_row = end_row; - end_row = temp; - - temp = start_col; - start_col = end_col; - end_col = temp; + if (col < length) + guac_terminal_display_render_glyph(display, row, col, + &characters[col], is_cursor, is_selected); + else + guac_terminal_display_render_glyph(display, row, col, + default_char, is_cursor, is_selected); } - /* First row */ - guac_protocol_send_rect(socket, select_layer, - - start_col * display->char_width, - start_row * display->char_height, - - display->width * display->char_width, - display->char_height); - - /* Middle */ - guac_protocol_send_rect(socket, select_layer, - - 0, - (start_row + 1) * display->char_height, - - display->width * display->char_width, - (end_row - start_row - 1) * display->char_height); - - /* Last row */ - guac_protocol_send_rect(socket, select_layer, - - 0, - end_row * display->char_height, - - (end_col + 1) * display->char_width, - display->char_height); - } - /* Draw new selection, erasing old */ - guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, - 0x00, 0x80, 0xFF, 0x60); - -} - -void guac_terminal_display_clear_select(guac_terminal_display* display) { - - /* Do nothing if nothing is selected */ - if (!display->text_selected) - return; - - guac_socket* socket = display->client->socket; - guac_layer* select_layer = display->select_layer; - - guac_protocol_send_rect(socket, select_layer, 0, 0, 1, 1); - guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, - 0x00, 0x00, 0x00, 0x00); - - /* Text is no longer selected */ - display->text_selected = false; - } int guac_terminal_display_set_font(guac_terminal_display* display, diff --git a/src/terminal/scrollbar.c b/src/terminal/scrollbar.c index 9a89474b2..8225365fb 100644 --- a/src/terminal/scrollbar.c +++ b/src/terminal/scrollbar.c @@ -20,23 +20,39 @@ #include "terminal/scrollbar.h" #include -#include +#include #include -#include #include -#include -#include +/** + * The opacity of the entire scrollbar, including both container and handle. The + * value 0x66 is 40% opacity. + */ +#define GUAC_TERMINAL_SCROLLBAR_OPACITY 0x66 + +/** + * The color to assign to the scrollbar handle (the component of the scrollbar + * that shows the current scroll position). + */ +#define GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR 0xFFFFFFFF + +/** + * The color to assign to the scrollbar container (the component of the + * scrollbar that contains the handle). + */ +#define GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR 0xFF808080 guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - const guac_layer* parent, int parent_width, int parent_height, int visible_area) { + guac_display* graphical_display, guac_display_layer* parent, + int parent_width, int parent_height, int visible_area) { /* Allocate scrollbar */ guac_terminal_scrollbar* scrollbar = guac_mem_alloc(sizeof(guac_terminal_scrollbar)); - /* Associate client */ + /* Associate client and corresponding display */ scrollbar->client = client; + scrollbar->graphical_display = graphical_display; /* Init default min/max and value */ scrollbar->min = 0; @@ -62,8 +78,17 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, scrollbar->render_state.container_height = 0; /* Allocate and init layers */ - scrollbar->container = guac_client_alloc_layer(client); - scrollbar->handle = guac_client_alloc_layer(client); + scrollbar->container = guac_display_alloc_layer(graphical_display, 1); + scrollbar->handle = guac_display_alloc_layer(graphical_display, 1); + + /* The parent layer contains the scrollbar container, while the container + * layer contains the scrollbar handle */ + guac_display_layer_set_parent(scrollbar->container, scrollbar->parent); + guac_display_layer_set_parent(scrollbar->handle, scrollbar->container); + + /* Use layer-level transparency to blend the scrollbar with the background + * color, rather than graphical updates leveraging the alpha channel */ + guac_display_layer_set_opacity(scrollbar->container, GUAC_TERMINAL_SCROLLBAR_OPACITY); /* Init mouse event state tracking */ scrollbar->dragging_handle = 0; @@ -79,8 +104,8 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /* Free layers */ - guac_client_free_layer(scrollbar->client, scrollbar->handle); - guac_client_free_layer(scrollbar->client, scrollbar->container); + guac_display_free_layer(scrollbar->handle); + guac_display_free_layer(scrollbar->container); /* Free scrollbar */ guac_mem_free(scrollbar); @@ -89,8 +114,8 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /** * Moves the main scrollbar layer to the position indicated within the given - * scrollbar render state, sending any necessary Guacamole instructions over - * the given socket. + * scrollbar render state, updating the underlying Guacamole display such that + * the new position will be sent to connected users for the next frame. * * @param scrollbar * The scrollbar to reposition. @@ -98,29 +123,20 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * position. - * - * @param socket - * The guac_socket over which any instructions necessary to perform the - * render operation should be sent. */ static void guac_terminal_scrollbar_move_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state, - guac_socket* socket) { + guac_terminal_scrollbar_render_state* state) { - /* Send scrollbar position */ - guac_protocol_send_move(socket, - scrollbar->container, scrollbar->parent, - state->container_x, - state->container_y, - 0); + guac_display_layer_move(scrollbar->container, + state->container_x, state->container_y); } /** * Resizes and redraws the main scrollbar layer according to the given - * scrollbar render state, sending any necessary Guacamole instructions over - * the given socket. + * scrollbar render state, updating the underlying Guacamole display such that + * the new position will be sent to connected users for the next frame. * * @param scrollbar * The scrollbar to resize and redraw. @@ -128,37 +144,40 @@ static void guac_terminal_scrollbar_move_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * size and appearance. - * - * @param socket - * The guac_socket over which any instructions necessary to perform the - * render operation should be sent. */ static void guac_terminal_scrollbar_draw_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state, - guac_socket* socket) { + guac_terminal_scrollbar_render_state* state) { /* Set container size */ - guac_protocol_send_size(socket, scrollbar->container, - state->container_width, - state->container_height); + guac_display_layer_resize(scrollbar->container, + state->container_width, state->container_height); /* Fill container with solid color */ - guac_protocol_send_rect(socket, scrollbar->container, 0, 0, - state->container_width, - state->container_height); - guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->container, - 0x80, 0x80, 0x80, 0x40); + guac_rect rect = { + .left = 0, + .top = 0, + .right = state->container_width, + .bottom = state->container_height + }; + + guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->container); + + guac_rect_constrain(&rect, &context->bounds); + guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR); + guac_rect_extend(&context->dirty, &rect); + + guac_display_layer_close_raw(scrollbar->container, context); } /** * Moves the handle layer of the scrollbar to the position indicated within the - * given scrollbar render state, sending any necessary Guacamole instructions - * over the given socket. The handle is the portion of the scrollbar that - * indicates the current scroll value and which the user can click and drag to - * change the value. + * given scrollbar render state, updating the underlying Guacamole display such + * that the new position will be sent to connected users for the next frame. The + * handle is the portion of the scrollbar that indicates the current scroll + * value and which the user can click and drag to change the value. * * @param scrollbar * The scrollbar associated with the handle being repositioned. @@ -166,31 +185,21 @@ static void guac_terminal_scrollbar_draw_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle position. - * - * @param socket - * The guac_socket over which any instructions necessary to perform the - * render operation should be sent. */ static void guac_terminal_scrollbar_move_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state, - guac_socket* socket) { + guac_terminal_scrollbar_render_state* state) { - /* Send handle position */ - guac_protocol_send_move(socket, - scrollbar->handle, scrollbar->container, - state->handle_x, - state->handle_y, - 0); + guac_display_layer_move(scrollbar->handle, state->handle_x, state->handle_y); } /** * Resizes and redraws the handle layer of the scrollbar according to the given - * scrollbar render state, sending any necessary Guacamole instructions over - * the given socket. The handle is the portion of the scrollbar that indicates - * the current scroll value and which the user can click and drag to change the - * value. + * scrollbar render state, updating the underlying Guacamole display such + * that the new position will be sent to connected users for the next frame. The + * handle is the portion of the scrollbar that indicates the current scroll + * value and which the user can click and drag to change the value. * * @param scrollbar * The scrollbar associated with the handle being resized and redrawn. @@ -198,28 +207,31 @@ static void guac_terminal_scrollbar_move_handle( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle size and appearance. - * - * @param socket - * The guac_socket over which any instructions necessary to perform the - * render operation should be sent. */ static void guac_terminal_scrollbar_draw_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state, - guac_socket* socket) { + guac_terminal_scrollbar_render_state* state) { /* Set handle size */ - guac_protocol_send_size(socket, scrollbar->handle, - state->handle_width, - state->handle_height); + guac_display_layer_resize(scrollbar->handle, + state->handle_width, state->handle_height); /* Fill handle with solid color */ - guac_protocol_send_rect(socket, scrollbar->handle, 0, 0, - state->handle_width, - state->handle_height); - guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->handle, - 0xA0, 0xA0, 0xA0, 0x8F); + guac_rect rect = { + .left = 0, + .top = 0, + .right = state->handle_width, + .bottom = state->handle_height + }; + + guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->handle); + + guac_rect_constrain(&rect, &context->bounds); + guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR); + guac_rect_extend(&context->dirty, &rect); + + guac_display_layer_close_raw(scrollbar->handle, context); } @@ -333,26 +345,8 @@ static void calculate_state(guac_terminal_scrollbar* scrollbar, } -void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, - guac_client* client, guac_socket* socket) { - - /* Get old state */ - guac_terminal_scrollbar_render_state* state = &scrollbar->render_state; - - /* Send scrollbar container */ - guac_terminal_scrollbar_draw_container(scrollbar, state, socket); - guac_terminal_scrollbar_move_container(scrollbar, state, socket); - - /* Send handle */ - guac_terminal_scrollbar_draw_handle(scrollbar, state, socket); - guac_terminal_scrollbar_move_handle(scrollbar, state, socket); - -} - void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { - guac_socket* socket = scrollbar->client->socket; - /* Get old state */ int old_value = scrollbar->value; guac_terminal_scrollbar_render_state* old_state = &scrollbar->render_state; @@ -369,25 +363,25 @@ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { /* Reposition container if moved */ if (old_state->container_x != new_state.container_x || old_state->container_y != new_state.container_y) { - guac_terminal_scrollbar_move_container(scrollbar, &new_state, socket); + guac_terminal_scrollbar_move_container(scrollbar, &new_state); } /* Resize and redraw container if size changed */ if (old_state->container_width != new_state.container_width || old_state->container_height != new_state.container_height) { - guac_terminal_scrollbar_draw_container(scrollbar, &new_state, socket); + guac_terminal_scrollbar_draw_container(scrollbar, &new_state); } /* Reposition handle if moved */ if (old_state->handle_x != new_state.handle_x || old_state->handle_y != new_state.handle_y) { - guac_terminal_scrollbar_move_handle(scrollbar, &new_state, socket); + guac_terminal_scrollbar_move_handle(scrollbar, &new_state); } /* Resize and redraw handle if size changed */ if (old_state->handle_width != new_state.handle_width || old_state->handle_height != new_state.handle_height) { - guac_terminal_scrollbar_draw_handle(scrollbar, &new_state, socket); + guac_terminal_scrollbar_draw_handle(scrollbar, &new_state); } /* Store current render state */ diff --git a/src/terminal/select.c b/src/terminal/select.c index 20bf9b5d2..62c03dc3f 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -76,7 +76,7 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, *start_row = terminal->selection_start_row; *start_col = terminal->selection_start_column; *end_row = terminal->selection_end_row; - *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; + *end_col = terminal->selection_end_column; } @@ -84,106 +84,21 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, * final character width */ else { *end_row = terminal->selection_start_row; - *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; + *end_col = terminal->selection_start_column; *start_row = terminal->selection_end_row; *start_col = terminal->selection_end_column; } } -void guac_terminal_select_redraw(guac_terminal* terminal) { - - /* Update the selected region of the display if text is currently - * selected */ - if (terminal->text_selected) { - - int start_row = terminal->selection_start_row + terminal->scroll_offset; - int start_column = terminal->selection_start_column; - - int end_row = terminal->selection_end_row + terminal->scroll_offset; - int end_column = terminal->selection_end_column; - - /* Update start/end columns to include character width */ - if (start_row > end_row || (start_row == end_row && start_column > end_column)) - start_column += terminal->selection_start_width - 1; - else - end_column += terminal->selection_end_width - 1; - - guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); - - } - - /* Clear the display selection if no text is currently selected */ - else - guac_terminal_display_clear_select(terminal->display); - -} - -/** - * Locates the beginning of the character at the given row and column, updating - * the column to the starting column of that character. The width, if available, - * is returned. If the character has no defined width, 1 is returned. - * - * @param terminal - * The guac_terminal in which the character should be located. - * - * @param row - * The row number of the desired character, 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 column - * A pointer to an int containing the column number of the desired - * character, where 0 is the first (left-most) column within the row. If - * the character is a multi-column character, the value of this int will be - * adjusted as necessary such that it contains the column number of the - * first column containing the character. - * - * @return - * The width of the specified character, in columns, or 1 if the character - * has no defined width. - */ -static int guac_terminal_find_char(guac_terminal* terminal, - int row, int* column) { - - 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) { - - /* Find beginning of character */ - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); - while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { - start_char--; - start_column--; - } - - /* Use width, if available */ - if (start_char->value != GUAC_CHAR_CONTINUATION) { - *column = start_column; - return start_char->width; - } - - } - - /* Default to one column wide */ - return 1; - -} - void guac_terminal_select_start(guac_terminal* terminal, int row, int column) { - int width = guac_terminal_find_char(terminal, row, &column); - terminal->selection_start_row = terminal->selection_end_row = row; terminal->selection_start_column = terminal->selection_end_column = column; - terminal->selection_start_width = - terminal->selection_end_width = width; - terminal->text_selected = false; terminal->selection_committed = false; guac_terminal_notify(terminal); @@ -194,14 +109,11 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { /* Only update if selection has changed */ if (row != terminal->selection_end_row - || column <= terminal->selection_end_column - || column >= terminal->selection_end_column + terminal->selection_end_width) { - - int width = guac_terminal_find_char(terminal, row, &column); + || column <= terminal->selection_end_column + || column >= terminal->selection_end_column) { terminal->selection_end_row = row; terminal->selection_end_column = column; - terminal->selection_end_width = width; terminal->text_selected = true; guac_terminal_notify(terminal); @@ -251,21 +163,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 +187,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 +269,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); + guac_terminal_char* characters; + bool last_row_was_wrapped = true; - /* 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++) { - - /* 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..9d8e4ff99 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -18,7 +18,6 @@ */ #include "common/clipboard.h" -#include "common/cursor.h" #include "common/iconv.h" #include "terminal/buffer.h" #include "terminal/color-scheme.h" @@ -34,6 +33,7 @@ #include #include +#include #include #include #include @@ -45,111 +45,13 @@ #include #include +#include #include #include #include #include #include - -/** - * Sets the given range of columns to the given character. - */ -static void __guac_terminal_set_columns(guac_terminal* terminal, int row, - int start_column, int end_column, guac_terminal_char* character) { - - guac_terminal_display_set_columns(terminal->display, row + terminal->scroll_offset, - start_column, end_column, character); - - guac_terminal_buffer_set_columns(terminal->buffer, row, - start_column, end_column, character); - - /* Clear selection if region is modified */ - guac_terminal_select_touch(terminal, row, start_column, row, end_column); - -} - -/** - * 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); - - } - - } - -} +#include /** * Returns the number of rows available within the terminal buffer, taking @@ -177,13 +79,7 @@ static int guac_terminal_effective_buffer_length(guac_terminal* term) { else if (scrollback < term->term_height) scrollback = term->term_height; - /* 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); } @@ -210,13 +106,12 @@ void guac_terminal_reset(guac_terminal* term) { term->char_mapping[1] = NULL; /* Reset cursor location */ - term->cursor_row = term->visible_cursor_row = term->saved_cursor_row = 0; - term->cursor_col = term->visible_cursor_col = term->saved_cursor_col = 0; + term->cursor_row = term->saved_cursor_row = 0; + term->cursor_col = term->saved_cursor_col = 0; 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; @@ -258,28 +153,37 @@ void guac_terminal_reset(guac_terminal* term) { * * @param terminal * The terminal whose background should be painted or repainted. - * - * @param socket - * The socket over which instructions required to paint / repaint the - * terminal background should be send. */ -static void guac_terminal_repaint_default_layer(guac_terminal* terminal, - guac_socket* socket) { +static void guac_terminal_repaint_default_layer(guac_terminal* terminal) { int width = terminal->width; int height = terminal->height; guac_terminal_display* display = terminal->display; - - /* Get background color */ - const guac_terminal_color* color = &display->default_background; + guac_display_layer* default_layer = guac_display_default_layer(terminal->graphical_display); /* Reset size */ - guac_protocol_send_size(socket, GUAC_DEFAULT_LAYER, width, height); + guac_display_layer_resize(default_layer, width, height); /* Paint background color */ - guac_protocol_send_rect(socket, GUAC_DEFAULT_LAYER, 0, 0, width, height); - guac_protocol_send_cfill(socket, GUAC_COMP_OVER, GUAC_DEFAULT_LAYER, - color->red, color->green, color->blue, 0xFF); + guac_rect rect = { + .left = 0, + .top = 0, + .right = width, + .bottom = height + }; + + uint32_t background = 0xFF000000 + | (display->default_background.red << 16) + | (display->default_background.green << 8) + | display->default_background.blue; + + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + + guac_rect_constrain(&rect, &context->bounds); + guac_display_layer_raw_context_set(context, &rect, background); + guac_rect_extend(&context->dirty, &rect); + + guac_display_layer_close_raw(default_layer, context); } @@ -306,10 +210,6 @@ void* guac_terminal_thread(void* data) { if (guac_terminal_render_frame(terminal)) break; - /* Signal end of frame */ - guac_client_end_frame(client); - guac_socket_flush(client->socket); - } /* The client has stopped or an error has occurred */ @@ -469,9 +369,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,13 +382,12 @@ 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, + term->graphical_display = guac_display_alloc(client); + term->display = guac_terminal_display_alloc(client, term->graphical_display, options->font_name, options->font_size, options->dpi, &default_char.attributes.foreground, &default_char.attributes.background, @@ -503,9 +400,6 @@ guac_terminal* guac_terminal_create(guac_client* client, return NULL; } - /* Init common cursor */ - term->cursor = guac_common_cursor_alloc(client); - /* Init terminal state */ term->current_attributes = default_char.attributes; term->default_char = default_char; @@ -555,12 +449,13 @@ guac_terminal* guac_terminal_create(guac_client* client, pthread_mutex_init(&(term->lock), NULL); /* Repaint and resize overall display */ - guac_terminal_repaint_default_layer(term, term->client->socket); + guac_terminal_repaint_default_layer(term); guac_terminal_display_resize(term->display, term->term_width, term->term_height); /* Allocate scrollbar */ - term->scrollbar = guac_terminal_scrollbar_alloc(term->client, GUAC_DEFAULT_LAYER, + term->scrollbar = guac_terminal_scrollbar_alloc(term->client, term->graphical_display, + guac_display_default_layer(term->graphical_display), term->outer_width, term->outer_height, term->term_height); /* Associate scrollbar with this terminal */ @@ -579,10 +474,6 @@ guac_terminal* guac_terminal_create(guac_client* client, term->mod_meta = term->mod_shift = 0; - /* Initialize mouse cursor */ - term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; - guac_common_cursor_set_blank(term->cursor); - /* Start terminal thread */ if (pthread_create(&(term->thread), NULL, guac_terminal_thread, (void*) term)) { @@ -633,16 +524,16 @@ 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); + guac_display_free(term->graphical_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 +543,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 +566,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; + int retval = guac_flag_timedwait_and_lock(&terminal->modified, + GUAC_TERMINAL_MODIFIED, msec_timeout); - /* 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; - -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 +589,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 +626,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); } @@ -903,51 +733,6 @@ 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); - } - - /* 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); - - term->visible_cursor_row = term->cursor_row; - term->visible_cursor_col = term->cursor_col; - } - - /* Otherwise set visible position to a sentinel value */ - else { - term->visible_cursor_row = -1; - term->visible_cursor_col = -1; - } - - return; - -} - int guac_terminal_write(guac_terminal* term, const char* buffer, int length) { guac_terminal_lock(term); @@ -971,33 +756,25 @@ 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) { - /* Scroll up visibly */ - 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, -guac_terminal_get_available_scroll(term), 0); - /* Update cursor location if within region */ - if (term->visible_cursor_row >= start_row && - term->visible_cursor_row <= end_row) - term->visible_cursor_row -= amount; - /* Update selected region */ if (term->text_selected) { term->selection_start_row -= amount; @@ -1015,14 +792,9 @@ int guac_terminal_scroll_up(guac_terminal* term, end_row - amount + 1, 0, end_row, term->term_width - 1); - /* Flush display copy before the cursor commit override operation - * 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); @@ -1032,11 +804,6 @@ int guac_terminal_scroll_down(guac_terminal* term, start_row, 0, start_row + amount - 1, term->term_width - 1); - /* Flush display copy before the cursor commit override operation - * type for visible cursor row and breaks display. */ - guac_terminal_display_flush(term->display); - - return 0; } int guac_terminal_clear_columns(guac_terminal* term, @@ -1099,53 +866,9 @@ int guac_terminal_clear_range(guac_terminal* term, } -/** - * Returns whether the given character would be visible relative to the - * background of the given terminal. - * - * @param term - * The guac_terminal to test the character against. - * - * @param c - * The character being tested. - * - * @return - * true if the given character is different from the terminal background, - * false otherwise. - */ -static bool guac_terminal_is_visible(guac_terminal* term, - guac_terminal_char* c) { - - /* Continuation characters are NEVER visible */ - if (c->value == GUAC_CHAR_CONTINUATION) - return false; - - /* Characters with glyphs are ALWAYS visible */ - if (guac_terminal_has_glyph(c->value)) - return true; - - const guac_terminal_color* background; - - /* Determine actual background color of character */ - if (c->attributes.reverse != c->attributes.cursor) - background = &c->attributes.foreground; - else - background = &c->attributes.background; - - /* Blank characters are visible if their background color differs from that - * of the terminal */ - return guac_terminal_colorcmp(background, - &term->default_char.attributes.background) != 0; - -} - void guac_terminal_scroll_display_down(guac_terminal* terminal, int scroll_amount) { - int start_row, end_row; - int dest_row; - int row, column; - /* Limit scroll amount by size of scrollback buffer */ if (scroll_amount > terminal->scroll_offset) scroll_amount = terminal->scroll_offset; @@ -1154,49 +877,10 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, if (scroll_amount <= 0) return; - /* Shift screen up */ - if (terminal->term_height > scroll_amount) - guac_terminal_display_copy_rows(terminal->display, - scroll_amount, terminal->term_height - 1, - -scroll_amount); - /* Advance by scroll amount */ terminal->scroll_offset -= scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); - /* Get row range */ - end_row = terminal->term_height - terminal->scroll_offset - 1; - start_row = end_row - scroll_amount + 1; - dest_row = terminal->term_height - scroll_amount; - - /* Draw new rows from scrollback */ - 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); - - /* 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++) { - - /* Only draw if not blank */ - if (guac_terminal_is_visible(terminal, current)) - guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); - - current++; - - } - - /* Next row */ - dest_row++; - - } - guac_terminal_notify(terminal); } @@ -1204,10 +888,6 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, void guac_terminal_scroll_display_up(guac_terminal* terminal, int scroll_amount) { - int start_row, end_row; - int dest_row; - int row, column; - /* Limit scroll amount by size of scrollback buffer */ int available_scroll = guac_terminal_get_available_scroll(terminal); if (terminal->scroll_offset + scroll_amount > available_scroll) @@ -1217,49 +897,10 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, if (scroll_amount <= 0) return; - /* Shift screen down */ - if (terminal->term_height > scroll_amount) - guac_terminal_display_copy_rows(terminal->display, - 0, terminal->term_height - scroll_amount - 1, - scroll_amount); - /* Advance by scroll amount */ terminal->scroll_offset += scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); - /* Get row range */ - start_row = -terminal->scroll_offset; - end_row = start_row + scroll_amount - 1; - dest_row = 0; - - /* Draw new rows from scrollback */ - 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); - - /* 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++) { - - /* Only draw if not blank */ - if (guac_terminal_is_visible(terminal, current)) - guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); - - current++; - - } - - /* Next row */ - dest_row++; - - } - guac_terminal_notify(terminal); } @@ -1267,103 +908,49 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, void guac_terminal_copy_columns(guac_terminal* terminal, int row, int start_column, int end_column, int offset) { - 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 */ guac_terminal_select_touch(terminal, row, start_column, row, end_column); - /* Update cursor location if within region */ - if (row == terminal->visible_cursor_row && - terminal->visible_cursor_col >= start_column && - 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, int start_row, int end_row, int offset) { - 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 */ guac_terminal_select_touch(terminal, start_row, 0, end_row, terminal->term_width); - /* Update cursor location if within region */ - if (terminal->visible_cursor_row >= start_row && - terminal->visible_cursor_row <= end_row) - terminal->visible_cursor_row += offset; - } void guac_terminal_set_columns(guac_terminal* terminal, int row, int start_column, int end_column, guac_terminal_char* character) { - __guac_terminal_set_columns(terminal, row, start_column, end_column, character); - - /* If visible cursor in current row, preserve state */ - if (row == terminal->visible_cursor_row - && terminal->visible_cursor_col >= start_column - && terminal->visible_cursor_col <= end_column) { - - /* Create copy of character with cursor attribute set */ - guac_terminal_char cursor_character = *character; - cursor_character.attributes.cursor = true; - - __guac_terminal_set_columns(terminal, row, - terminal->visible_cursor_col, terminal->visible_cursor_col, &cursor_character); - - } - - /* 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) { - - int row, col; - - /* 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); - - /* 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++) { - - /* Only redraw if not blank */ - guac_terminal_char* c = &(buffer_row->characters[col]); - if (guac_terminal_is_visible(term, c)) - guac_terminal_display_set_columns(term->display, row, col, col, c); - - } + guac_terminal_buffer_set_columns(terminal->current_buffer, row, + start_column, end_column, character); - } + /* Clear selection if region is modified */ + guac_terminal_select_touch(terminal, row, start_column, row, end_column); } /** * 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) { @@ -1382,30 +969,17 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { /* If the new terminal bottom covers N rows, shift up N rows */ if (shift_amount > 0) { - guac_terminal_display_copy_rows(term->display, - shift_amount, term->display->height - 1, -shift_amount); - /* Update buffer top and cursor row based on shift */ - term->buffer->top += shift_amount; - term->cursor_row -= shift_amount; - if (term->visible_cursor_row != -1) - term->visible_cursor_row -= shift_amount; - - /* Redraw characters within old region */ - __guac_terminal_redraw_rect(term, height - shift_amount, 0, height-1, width-1); + guac_terminal_buffer_scroll_up(term->current_buffer, shift_amount); + term->cursor_row -= shift_amount; } } /* Resize display */ - guac_terminal_display_flush(term->display); guac_terminal_display_resize(term->display, width, height); - /* Redraw any characters on right if widening */ - if (width > term->term_width) - __guac_terminal_redraw_rect(term, 0, term->term_width-1, height-1, width-1); - /* If height is increasing, shift display down */ if (height > term->term_height) { @@ -1421,67 +995,37 @@ 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; - term->cursor_row += shift_amount; - if (term->visible_cursor_row != -1) - term->visible_cursor_row += shift_amount; + guac_terminal_buffer_scroll_down(term->current_buffer, shift_amount); + term->cursor_row += shift_amount; /* If scrolled enough, use scroll to fulfill entire resize */ if (term->scroll_offset >= shift_amount) { - term->scroll_offset -= shift_amount; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); - - /* Draw characters from scroll at bottom */ - __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + shift_amount - 1, width-1); - } /* Otherwise, fulfill with as much scroll as possible */ else { - - /* Draw characters from scroll at bottom */ - __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + term->scroll_offset - 1, width-1); - - /* Update shift_amount and scroll based on new rows */ shift_amount -= term->scroll_offset; term->scroll_offset = 0; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); - - /* If anything remains, move screen as necessary */ - if (shift_amount > 0) { - - guac_terminal_display_copy_rows(term->display, - 0, term->display->height - shift_amount - 1, shift_amount); - - /* Draw characters at top from scroll */ - __guac_terminal_redraw_rect(term, 0, 0, shift_amount - 1, width-1); - - } - } } /* end if undisplayed rows exist */ } - /* Keep cursor on screen */ - if (term->cursor_row < 0) term->cursor_row = 0; - if (term->cursor_row >= height) term->cursor_row = height-1; - if (term->cursor_col < 0) term->cursor_col = 0; - if (term->cursor_col >= width) term->cursor_col = width-1; - /* Commit new dimensions */ term->term_width = width; term->term_height = height; + /* Reset scroll region */ + term->scroll_end = height - 1; + } int guac_terminal_resize(guac_terminal* terminal, int width, int height) { - guac_terminal_display* display = terminal->display; - guac_client* client = display->client; - /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1504,15 +1048,11 @@ int guac_terminal_resize(guac_terminal* terminal, int width, int height) { terminal->width = adjusted_width; /* Resize default layer to given pixel dimensions */ - guac_terminal_repaint_default_layer(terminal, client->socket); + guac_terminal_repaint_default_layer(terminal); /* Resize terminal if row/column dimensions have changed */ if (columns != terminal->term_width || rows != terminal->term_height) { - /* Resize terminal and set the columns and rows on the terminal struct */ __guac_terminal_resize(terminal, columns, rows); - - /* Reset scroll region */ - terminal->scroll_end = rows - 1; } /* Notify scrollbar of resize */ @@ -1540,11 +1080,18 @@ void guac_terminal_flush(guac_terminal* terminal) { guac_terminal_pipe_stream_flush(terminal); /* Flush display state */ - guac_terminal_select_redraw(terminal); - guac_terminal_commit_cursor(terminal); - guac_terminal_display_flush(terminal->display); guac_terminal_scrollbar_flush(terminal->scrollbar); + guac_terminal_display_render_buffer(terminal->display, + terminal->current_buffer, terminal->scroll_offset, + &terminal->default_char, + terminal->cursor_visible, terminal->cursor_row, terminal->cursor_col, + terminal->text_selected, terminal->selection_start_row, terminal->selection_start_column, + terminal->selection_end_row, terminal->selection_end_column); + + guac_display_end_multiple_frames(terminal->graphical_display, 1); + guac_socket_flush(terminal->client->socket); + } void guac_terminal_lock(guac_terminal* terminal) { @@ -1585,11 +1132,8 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed } /* Hide mouse cursor if not already hidden */ - if (term->current_cursor != GUAC_TERMINAL_CURSOR_BLANK) { - term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; - guac_common_cursor_set_blank(term->cursor); - guac_terminal_notify(term); - } + guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_BLANK); + guac_terminal_notify(term); /* Track modifiers */ if (keysym == 0xFFE3 || keysym == 0xFFE4) @@ -1809,28 +1353,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 +1375,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" */ @@ -1906,17 +1438,13 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int pressed_mask = ~term->mouse_mask & mask; /* Store current mouse location/state */ - guac_common_cursor_update(term->cursor, user, x, y, mask); + guac_display_notify_user_moved_mouse(term->graphical_display, user, x, y, mask); /* Notify scrollbar, do not handle anything handled by scrollbar */ if (guac_terminal_scrollbar_handle_mouse(term->scrollbar, x, y, mask)) { /* Set pointer cursor if mouse is over scrollbar */ - if (term->current_cursor != GUAC_TERMINAL_CURSOR_POINTER) { - term->current_cursor = GUAC_TERMINAL_CURSOR_POINTER; - guac_common_cursor_set_pointer(term->cursor); - guac_terminal_notify(term); - } + guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_POINTER); guac_terminal_notify(term); return 0; @@ -1930,11 +1458,8 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, term->mouse_mask = mask; /* Show mouse cursor if not already shown */ - if (term->current_cursor != GUAC_TERMINAL_CURSOR_IBAR) { - term->current_cursor = GUAC_TERMINAL_CURSOR_IBAR; - guac_common_cursor_set_ibar(term->cursor); - guac_terminal_notify(term); - } + guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_IBAR); + guac_terminal_notify(term); /* Paste contents of clipboard on right or middle mouse button up */ if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE)) @@ -2227,47 +1752,19 @@ int guac_terminal_create_typescript(guac_terminal* term, const char* path, } -/** - * Synchronize the state of the provided terminal to a subset of users of - * the provided guac_client using the provided socket. - * - * @param client - * The client whose users should be synchronized. - * - * @param term - * The terminal state that should be synchronized to the users. - * - * @param socket - * The socket that should be used to communicate with the users. - */ -static void __guac_terminal_sync_socket( - guac_client* client, guac_terminal* term, guac_socket* socket) { - - /* Synchronize display state with new user */ - guac_terminal_repaint_default_layer(term, socket); - guac_terminal_display_dup(term->display, client, socket); - - /* Synchronize mouse cursor */ - guac_common_cursor_dup(term->cursor, client, socket); - - /* Paint scrollbar for joining users */ - guac_terminal_scrollbar_dup(term->scrollbar, client, socket); - -} - void guac_terminal_dup(guac_terminal* term, guac_user* user, guac_socket* socket) { - /* Ignore the user and just use the provided socket directly */ - __guac_terminal_sync_socket(user->client, term, socket); + /* Synchronize state to any users on given socket */ + guac_display_dup(term->graphical_display, socket); } -void guac_terminal_sync_users( - guac_terminal* term, guac_client* client, guac_socket* socket) { +void guac_terminal_sync_users(guac_terminal* term, guac_client* client, + guac_socket* socket) { - /* Use the provided socket to synchronize state to the users */ - __guac_terminal_sync_socket(client, term, socket); + /* Synchronize state to any users on given socket */ + guac_display_dup(term->graphical_display, socket); } @@ -2290,10 +1787,7 @@ void guac_terminal_apply_color_scheme(guac_terminal* terminal, display->default_background = default_char->attributes.background; /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal, client->socket); - __guac_terminal_redraw_rect(terminal, 0, 0, - terminal->term_height - 1, - terminal->term_width - 1); + guac_terminal_repaint_default_layer(terminal); /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -2316,7 +1810,6 @@ const char* guac_terminal_get_color_scheme(guac_terminal* terminal) { void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, int font_size, int dpi) { - guac_client* client = terminal->client; guac_terminal_display* display = terminal->display; if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) @@ -2327,12 +1820,6 @@ void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, guac_terminal_resize(terminal, terminal->outer_width, terminal->outer_height); - /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal, client->socket); - __guac_terminal_redraw_rect(terminal, 0, 0, - terminal->term_height - 1, - terminal->term_width - 1); - /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -2393,72 +1880,5 @@ void guac_terminal_clipboard_append(guac_terminal* terminal, } 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; - - } - + guac_display_notify_user_left(terminal->graphical_display, user); } diff --git a/src/terminal/terminal/buffer.h b/src/terminal/terminal/buffer.h index cebb8e88e..2e974d6a5 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,57 @@ 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); + #endif diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index 635536088..30b4e9e02 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -26,11 +26,13 @@ * @file display.h */ -#include "common/surface.h" +#include "buffer.h" #include "palette.h" +#include "terminal.h" #include "types.h" #include +#include #include #include @@ -54,74 +56,18 @@ #define GUAC_TERMINAL_MM_PER_INCH 25.4 /** - * All available terminal operations which affect character cells. - */ -typedef enum guac_terminal_operation_type { - - /** - * Operation which does nothing. - */ - GUAC_CHAR_NOP, - - /** - * Operation which copies a character from a given row/column coordinate. - */ - GUAC_CHAR_COPY, - - /** - * Operation which sets the character and attributes. - */ - GUAC_CHAR_SET - -} guac_terminal_operation_type; - -/** - * A pairing of a guac_terminal_operation_type and all parameters required by - * that operation type. - */ -typedef struct guac_terminal_operation { - - /** - * The type of operation to perform. - */ - guac_terminal_operation_type type; - - /** - * The character (and attributes) to set the current location to. This is - * only applicable to GUAC_CHAR_SET. - */ - guac_terminal_char character; - - /** - * The row to copy a character from. This is only applicable to - * GUAC_CHAR_COPY. - */ - int row; - - /** - * The column to copy a character from. This is only applicable to - * GUAC_CHAR_COPY. - */ - int column; - -} guac_terminal_operation; - -/** - * Set of all pending operations for the currently-visible screen area, and the - * contextual information necessary to interpret and render those changes. + * The rendering area and state of the text display used by the terminal + * emulator. The actual changes between successive frames are tracked by an + * underlying guac_display. */ typedef struct guac_terminal_display { /** - * The Guacamole client this display will use for rendering. + * The Guacamole client associated with this terminal emulator having this + * display. */ guac_client* client; - /** - * Array of all operations pending for the visible screen area. - */ - guac_terminal_operation* operations; - /** * The width of the screen, in characters. */ @@ -137,6 +83,18 @@ typedef struct guac_terminal_display { */ int margin; + /** + * The current mouse cursor (the mouse cursor already sent to connected + * users), to avoid re-setting the cursor image when effectively no change + * has been made. + */ + guac_terminal_cursor_type current_cursor; + + /** + * The mouse cursor that was most recently requested. + */ + guac_terminal_cursor_type last_requested_cursor; + /** * The description of the font to use for rendering. */ @@ -174,64 +132,50 @@ typedef struct guac_terminal_display { guac_terminal_color default_background; /** - * The foreground color to be used for the next glyph rendered to the - * terminal. - */ - guac_terminal_color glyph_foreground; - - /** - * The background color to be used for the next glyph rendered to the - * terminal. - */ - guac_terminal_color glyph_background; - - /** - * The surface containing the actual terminal. + * The Guacamole display that this terminal emulator should render to. */ - guac_common_surface* display_surface; + guac_display* graphical_display; /** * Layer which contains the actual terminal. */ - guac_layer* display_layer; - - /** - * Sub-layer of display layer which highlights selected text. - */ - guac_layer* select_layer; - - /** - * Whether text is currently selected. - */ - bool text_selected; - - /** - * The row that the selection starts at. - */ - int selection_start_row; - - /** - * The column that the selection starts at. - */ - int selection_start_column; - - /** - * The row that the selection ends at. - */ - int selection_end_row; - - /** - * The column that the selection ends at. - */ - int selection_end_column; + guac_display_layer* display_layer; } guac_terminal_display; /** - * Allocates a new display having the given default foreground and background - * colors. + * Allocates a new display having the given text rendering properties and + * underlying graphical display. + * + * @param client + * The guac_client associated with the terminal session. + * + * @param graphical_display + * The guac_display that the new guac_terminal_display should render to. + * + * @param font_name + * The name of the font to use to render characters. + * + * @param font_size + * The font size to use when rendering characters, in points. + * + * @param dpi + * The resolution that characters should be rendered at, in DPI (dots per + * inch). + * + * @param foreground + * The default foreground color to use for characters rendered to the + * display. + * + * @param background + * The default background color to use for characters rendered to the + * display. + * + * @param palette + * The palette to use for all other colors supported by the terminal. */ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, + guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]); @@ -290,71 +234,11 @@ int guac_terminal_display_assign_color(guac_terminal_display* display, int guac_terminal_display_lookup_color(guac_terminal_display* display, int index, guac_terminal_color* color); -/** - * Copies the given range of columns to a new location, offset from - * the original by the given number of columns. - */ -void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, - int start_column, int end_column, int offset); - -/** - * Copies the given range of rows to a new location, offset from the - * original by the given number of rows. - */ -void guac_terminal_display_copy_rows(guac_terminal_display* display, - int start_row, int end_row, int offset); - -/** - * Sets the given range of columns within the given row to the given - * character. - */ -void guac_terminal_display_set_columns(guac_terminal_display* display, int row, - int start_column, int end_column, guac_terminal_char* character); - /** * Resize the terminal to the given dimensions. */ void guac_terminal_display_resize(guac_terminal_display* display, int width, int height); -/** - * Flushes all pending operations within the given guac_terminal_display. - */ -void guac_terminal_display_flush(guac_terminal_display* display); - -/** - * Initializes and syncs the current terminal display state for all joining - * users associated with the provided socket, sending the necessary instructions - * to completely recreate and redraw the terminal rendering over the given - * socket. - * - * @param display - * The terminal display to sync to the users associated with the provided - * socket. - * - * @param client - * The client whose users are joining. - * - * @param socket - * The socket over which any necessary instructions should be sent. - */ -void guac_terminal_display_dup( - guac_terminal_display* display, guac_client* client, guac_socket* socket); - -/** - * Draws the text selection rectangle from the given coordinates to the given end coordinates. - */ -void guac_terminal_display_select(guac_terminal_display* display, - int start_row, int start_col, int end_row, int end_col); - -/** - * Clears the currently-selected region, removing the highlight. - * - * @param display - * The guac_terminal_display whose currently-selected region should be - * cleared. - */ -void guac_terminal_display_clear_select(guac_terminal_display* display); - /** * Alters the font of the terminal display. The available display area and the * regular grid of character cells will be resized as necessary to compensate @@ -385,5 +269,80 @@ void guac_terminal_display_clear_select(guac_terminal_display* display); int guac_terminal_display_set_font(guac_terminal_display* display, const char* font_name, int font_size, int dpi); +/** + * Renders the contents of the given buffer to the given terminal display. All + * characters within the buffer that fit within the display region will be + * rendered. + * + * @param display + * The terminal display receiving the buffer contents. + * + * @param buffer + * The buffer to render to the display. + * + * @param scroll_offset + * The number of rows from the scrollback buffer that the user has scrolled + * into view. + * + * @param default_char + * The character that should be used to populate any character cell that + * has not received any terminal output. + * + * @param cursor_visible + * Whether the cursor is currently visible (ie: has not been hidden using + * console codes that specifically hide the cursor). This does NOT refer to + * whether the cursor is within the display region, which is handled + * automatically. + * + * @oaran cursor_row + * The current row position of the cursor. + * + * @oaran cursor_col + * The current column position of the cursor. + * + * @param text_selected + * Whether the user has selected text. + * + * @param selection_start_row + * The row number where the user started their selection. This value only + * has meaning if text_selected is true. There is no requirement that the + * start row be less than the end row. + * + * @param selection_start_col + * The column number where the user started their selection. This value + * only has meaning if text_selected is true. There is no requirement that + * the start column be less than the end column. + * + * @param selection_end_row + * The row number where the user ended their selection. This value only has + * meaning if text_selected is true. There is no requirement that the end + * row be greated than the start row. + * + * @param selection_end_col + * The column number where the user ended their selection. This value only + * has meaning if text_selected is true. There is no requirement that the + * end column be greater than the start column. + */ +void guac_terminal_display_render_buffer(guac_terminal_display* display, + guac_terminal_buffer* buffer, int scroll_offset, + guac_terminal_char* default_char, + bool cursor_visible, int cursor_row, int cursor_col, + bool text_selected, int selection_start_row, int selection_start_col, + int selection_end_row, int selection_end_col); + +/** + * Set the mouse cursor icon. If different from the mouse cursor in effect at + * the time of the previous guac_display frame, the requested cursor will take + * effect the next time the terminal display is flushed. + * + * @param display + * The display to set the cursor of. + * + * @param cursor + * The cursor to assign. + */ +void guac_terminal_display_set_cursor(guac_terminal_display* display, + guac_terminal_cursor_type cursor); + #endif diff --git a/src/terminal/terminal/scrollbar.h b/src/terminal/terminal/scrollbar.h index d29e459c3..60fbe3780 100644 --- a/src/terminal/terminal/scrollbar.h +++ b/src/terminal/terminal/scrollbar.h @@ -27,7 +27,7 @@ */ #include -#include +#include /** * The width of the scrollbar, in pixels. @@ -97,6 +97,11 @@ typedef struct guac_terminal_scrollbar_render_state { } guac_terminal_scrollbar_render_state; +/** + * A scrollbar, made up of a containing layer and inner draggable handle. The + * position of the handle within the layer represents the value of the + * scrollbar. + */ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; /** @@ -106,11 +111,6 @@ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; typedef void guac_terminal_scrollbar_scroll_handler( guac_terminal_scrollbar* scrollbar, int value); -/** - * A scrollbar, made up of a containing layer and inner draggable handle. The - * position of the handle within the layer represents the value of the - * scrollbar. - */ struct guac_terminal_scrollbar { /** @@ -118,10 +118,15 @@ struct guac_terminal_scrollbar { */ guac_client* client; + /** + * The Guacamole display that this scrollbar should render to. + */ + guac_display* graphical_display; + /** * The layer containing the scrollbar. */ - const guac_layer* parent; + guac_display_layer* parent; /** * The width of the parent layer, in pixels. @@ -136,13 +141,13 @@ struct guac_terminal_scrollbar { /** * The scrollbar itself. */ - guac_layer* container; + guac_display_layer* container; /** * The draggable handle within the scrollbar, representing the current * scroll value. */ - guac_layer* handle; + guac_display_layer* handle; /** * The minimum scroll value. @@ -230,8 +235,8 @@ struct guac_terminal_scrollbar { * A newly allocated scrollbar. */ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - const guac_layer* parent, int parent_width, int parent_height, - int visible_area); + guac_display* graphical_display, guac_display_layer* parent, + int parent_width, int parent_height, int visible_area); /** * Frees the given scrollbar. @@ -253,24 +258,6 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar); */ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar); -/** - * Forces a complete redraw / resync of scrollbar state for all joining users - * associated with the provided socket, sending the necessary instructions to - * completely recreate and redraw the scrollbar rendering over the given - * socket. - * - * @param scrollbar - * The scrollbar to sync to the given users. - * - * @param client - * The client associated with the joining users. - * - * @param socket - * The socket over which any necessary instructions should be sent. - */ -void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, - guac_client* client, guac_socket* socket); - /** * Sets the minimum and maximum allowed scroll values of the given scrollbar * to the given values. If necessary, the current value of the scrollbar will diff --git a/src/terminal/terminal/select.h b/src/terminal/terminal/select.h index 8a2d96503..29e9d8b8f 100644 --- a/src/terminal/terminal/select.h +++ b/src/terminal/terminal/select.h @@ -30,18 +30,6 @@ #include -/** - * Forwards the visible portion of the text selection rectangle to the - * underlying terminal display, requesting that it be redrawn. If no - * visible change would result from redrawing the selection rectangle, - * this function may have no effect. - * - * @param terminal - * The guac_terminal whose text selection rectangle should be - * redrawn. - */ -void guac_terminal_select_redraw(guac_terminal* terminal); - /** * Marks the start of text selection at the given row and column. Any existing * selection is cleared. This function should only be invoked while the diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index cf9eb14c8..72e995de8 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -21,13 +21,22 @@ #define GUAC_TERMINAL_PRIV_H #include "common/clipboard.h" -#include "common/cursor.h" #include "buffer.h" #include "display.h" #include "scrollbar.h" #include "terminal.h" #include "typescript.h" +#include +#include +#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 @@ -51,6 +60,11 @@ struct guac_terminal { */ guac_client* client; + /** + * The Guacamole display that this terminal emulator should render to. + */ + guac_display* graphical_display; + /** * Whether user input should be handled and this terminal should render * frames. Initially, this will be false, user input will be ignored, and @@ -84,25 +98,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 + * will require a frame flush. + * + * @see GUAC_TERMINAL_MODIFIED */ - pthread_cond_t modified_cond; + guac_flag modified; /** * Pipe which will be the source of user input. When a terminal code @@ -155,11 +157,6 @@ struct guac_terminal { */ guac_terminal_typescript* typescript; - /** - * Terminal-wide mouse cursor, synchronized across all users. - */ - guac_common_cursor* cursor; - /** * Graphical representation of the current scroll state. */ @@ -243,11 +240,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,38 +249,11 @@ 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. */ bool cursor_visible; - /** - * The row of the rendered cursor. - * Will be set to -1 if the cursor is not visible. - */ - 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 +290,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 @@ -391,11 +364,6 @@ struct guac_terminal { */ int selection_start_column; - /** - * The width of the character at selection start. - */ - int selection_start_width; - /** * The row that the selection ends at. */ @@ -406,11 +374,6 @@ struct guac_terminal { */ int selection_end_column; - /** - * The width of the character at selection end. - */ - int selection_end_width; - /** * Whether the cursor (arrow) keys should send cursor sequences * or application sequences (DECCKM). @@ -452,11 +415,6 @@ struct guac_terminal { */ int mouse_mask; - /** - * The current mouse cursor, to avoid re-setting the cursor image. - */ - guac_terminal_cursor_type current_cursor; - /** * The current contents of the clipboard. This clipboard instance is * maintained externally (will not be freed when this terminal is freed) @@ -568,21 +526,15 @@ 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); -/** - * Commits the current cursor location, updating the visible cursor - * on the screen. - */ -void guac_terminal_commit_cursor(guac_terminal* term); - /** * Scroll down the display by the given amount, replacing the new space with * data from the buffer. If not enough data is available, the maximum @@ -698,18 +650,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/terminal.h b/src/terminal/terminal/terminal.h index 11c8fb5a3..bfeb49e76 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -78,13 +78,13 @@ /** * The maximum duration of a single frame, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_DURATION 40 +#define GUAC_TERMINAL_FRAME_DURATION 33 /** * The maximum amount of time to wait for more data before declaring a frame * complete, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_TIMEOUT 10 +#define GUAC_TERMINAL_FRAME_TIMEOUT 0 /** * The maximum number of custom tab stops. diff --git a/src/terminal/terminal/types.h b/src/terminal/terminal/types.h index 3e1f07468..460b4f640 100644 --- a/src/terminal/terminal/types.h +++ b/src/terminal/terminal/types.h @@ -54,29 +54,24 @@ 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). */ - bool reverse; - - /** - * Whether the associated character is highlighted by the cursor. - */ - bool cursor; + bool reverse : 1; /** * Whether to render the character with underscore. */ - bool underscore; + bool underscore : 1; /** * The foreground color of this character. From 3b6173f7bf1feeff1feaa74eeeb7bf58f0747907 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Sep 2024 15:43:07 -0700 Subject: [PATCH 22/53] GUACAMOLE-377: Work around libssh2 usage of strlen() on key data (otherwise encumbering testing of terminal emulator refactor). --- src/common-ssh/key.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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; From 40eef95f41c83af15152bf0f6dbf374c53ffb267 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 17 Aug 2024 19:53:55 -0700 Subject: [PATCH 23/53] GUACAMOLE-377: Increase maximum number of streams. The new guac_display otherwise tends to run out of outbound, client-wide streams. --- src/libguac/client.c | 3 +- src/libguac/guacamole/client-constants.h | 2 +- src/libguac/guacamole/pool.h | 59 +++++++++++- src/libguac/guacamole/user-constants.h | 2 +- src/libguac/pool.c | 112 +++++++++++++++++++---- src/libguac/user.c | 6 +- src/protocols/rdp/fs.c | 2 +- 7 files changed, 158 insertions(+), 28 deletions(-) diff --git a/src/libguac/client.c b/src/libguac/client.c index 508f175ee..e0783439b 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -132,7 +132,8 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { return NULL; /* Allocate stream */ - stream_index = guac_pool_next_int(client->__stream_pool); + stream_index = guac_pool_next_int_below_or_die(client->__stream_pool, + GUAC_CLIENT_MAX_STREAMS); /* Initialize stream with odd index (even indices are user-level) */ allocd_stream = &(client->__output_streams[stream_index]); 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/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/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/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/user.c b/src/libguac/user.c index 846d0eddd..c2b320428 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -112,7 +112,8 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { return NULL; /* Allocate stream */ - stream_index = guac_pool_next_int(user->__stream_pool); + stream_index = guac_pool_next_int_below_or_die(user->__stream_pool, + GUAC_USER_MAX_STREAMS); /* Initialize stream with even index (odd indices are client-level) */ allocd_stream = &(user->__output_streams[stream_index]); @@ -146,7 +147,8 @@ guac_object* guac_user_alloc_object(guac_user* user) { return NULL; /* Allocate object */ - object_index = guac_pool_next_int(user->__object_pool); + object_index = guac_pool_next_int_below_or_die(user->__object_pool, + GUAC_USER_MAX_OBJECTS); /* Initialize object */ allocd_object = &(user->__objects[object_index]); 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; From dc133eae7b0a0ddd13513f965cee3170e24c95e0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 15 Aug 2024 21:20:23 -0700 Subject: [PATCH 24/53] GUACAMOLE-377: Ignore directories and files used by Clang compilation database. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) 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 From ba5511cc549ed55d76ea3f0b72b8cea74a024e4b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 20 Aug 2024 10:52:34 -0700 Subject: [PATCH 25/53] GUACAMOLE-377: Correct return value confusion in handling of timeout vs. general failure of WaitForMultipleObjects(). The conversion of WAIT_FAILED to a signed int and back may cause the value of result to not actually match the value of the WAIT_FAILED macro due to the difference in size and sign extension during conversion. --- src/protocols/rdp/rdp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index c19892608..98f682e15 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -444,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 */ From c48409c352d79a52a66cb38ead2c7c7a548fb31f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 24 Aug 2024 16:35:15 -0700 Subject: [PATCH 26/53] GUACAMOLE-377: Prefer GUAC_COMP_OVER to GUAC_COMP_SRC for performance-critical operations (~3x faster). --- src/libguac/display-worker.c | 29 +++++++++++++++++++++++++---- src/libguac/display.c | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index c6b18bdf6..e39513bbc 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -23,6 +23,7 @@ #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" @@ -446,7 +447,8 @@ void* guac_display_worker_thread(void* data) { /* Allow connected clients to move forward with rendering */ guac_client_end_multiple_frames(client, display->last_frame.frames); - /* Commit any changed contents to client-side backing buffer */ + /* 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) { @@ -454,15 +456,34 @@ void* guac_display_worker_thread(void* data) { * been modified since the last frame */ guac_rect* dirty = ¤t->last_frame.dirty; if (!guac_rect_is_empty(dirty)) { - guac_protocol_send_copy(client->socket, current->layer, - 0, 0, current->last_frame.width, current->last_frame.height, - GUAC_COMP_SRC, current->last_frame_buffer, 0, 0); + + 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); diff --git a/src/libguac/display.c b/src/libguac/display.c index 2d627d4dd..731a44632 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -236,7 +236,7 @@ void guac_display_dup(guac_display* display, guac_socket* socket) { /* Resync copy of previous frame */ guac_protocol_send_copy(socket, layer, 0, 0, width, height, - GUAC_COMP_SRC, current->last_frame_buffer, 0, 0); + GUAC_COMP_OVER, current->last_frame_buffer, 0, 0); /* Resync any properties that are specific to non-buffer layers */ if (current->layer->index > 0) { From de30928a4d3df026bbb6af68f0fda76469717ae0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 28 Aug 2024 09:22:22 -0700 Subject: [PATCH 27/53] GUACAMOLE-377: Remove all old surface, display, cursor, and rect code from src/common/. --- src/common/Makefile.am | 20 +- src/common/blank_cursor.c | 75 - src/common/common/blank_cursor.h | 64 - src/common/common/cursor.h | 307 ---- src/common/common/display.h | 293 ---- src/common/common/dot_cursor.h | 60 - src/common/common/ibar_cursor.h | 62 - src/common/common/pointer_cursor.h | 60 - src/common/common/rect.h | 143 -- src/common/common/surface.h | 539 ------- src/common/cursor.c | 327 ---- src/common/display.c | 422 ----- src/common/dot_cursor.c | 85 - src/common/ibar_cursor.c | 98 -- src/common/pointer_cursor.c | 96 -- src/common/rect.c | 266 --- src/common/surface.c | 2041 ------------------------ src/common/tests/Makefile.am | 6 - src/common/tests/rect/clip_and_split.c | 156 -- src/common/tests/rect/constrain.c | 43 - src/common/tests/rect/expand_to_grid.c | 71 - src/common/tests/rect/extend.c | 42 - src/common/tests/rect/init.c | 39 - src/common/tests/rect/intersects.c | 91 -- src/protocols/vnc/user.c | 2 - 25 files changed, 2 insertions(+), 5406 deletions(-) delete mode 100644 src/common/blank_cursor.c delete mode 100644 src/common/common/blank_cursor.h delete mode 100644 src/common/common/cursor.h delete mode 100644 src/common/common/display.h delete mode 100644 src/common/common/dot_cursor.h delete mode 100644 src/common/common/ibar_cursor.h delete mode 100644 src/common/common/pointer_cursor.h delete mode 100644 src/common/common/rect.h delete mode 100644 src/common/common/surface.h delete mode 100644 src/common/cursor.c delete mode 100644 src/common/display.c delete mode 100644 src/common/dot_cursor.c delete mode 100644 src/common/ibar_cursor.c delete mode 100644 src/common/pointer_cursor.c delete mode 100644 src/common/rect.c delete mode 100644 src/common/surface.c delete mode 100644 src/common/tests/rect/clip_and_split.c delete mode 100644 src/common/tests/rect/constrain.c delete mode 100644 src/common/tests/rect/expand_to_grid.c delete mode 100644 src/common/tests/rect/extend.c delete mode 100644 src/common/tests/rect/init.c delete mode 100644 src/common/tests/rect/intersects.c diff --git a/src/common/Makefile.am b/src/common/Makefile.am index 947104aeb..c2be3f59d 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -31,36 +31,20 @@ SUBDIRS = . tests noinst_HEADERS = \ common/io.h \ - common/blank_cursor.h \ common/clipboard.h \ - common/cursor.h \ common/defaults.h \ - common/display.h \ - common/dot_cursor.h \ - common/ibar_cursor.h \ common/iconv.h \ common/json.h \ common/list.h \ - common/pointer_cursor.h \ - common/rect.h \ - common/string.h \ - common/surface.h + common/string.h libguac_common_la_SOURCES = \ io.c \ - blank_cursor.c \ clipboard.c \ - cursor.c \ - display.c \ - dot_cursor.c \ - ibar_cursor.c \ iconv.c \ json.c \ list.c \ - pointer_cursor.c \ - rect.c \ - string.c \ - surface.c + string.c libguac_common_la_CFLAGS = \ -Werror -Wall -pedantic \ diff --git a/src/common/blank_cursor.c b/src/common/blank_cursor.c deleted file mode 100644 index c65db0cbf..000000000 --- a/src/common/blank_cursor.c +++ /dev/null @@ -1,75 +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 "config.h" - -#include -#include -#include -#include -#include -#include - -/* Dimensions */ -const int guac_common_blank_cursor_width = 1; -const int guac_common_blank_cursor_height = 1; - -/* Format */ -const cairo_format_t guac_common_blank_cursor_format = CAIRO_FORMAT_ARGB32; -const int guac_common_blank_cursor_stride = 4; - -/* Embedded blank cursor graphic */ -unsigned char guac_common_blank_cursor[] = { - - 0x00,0x00,0x00,0x00 - -}; - -void guac_common_set_blank_cursor(guac_user* user) { - - guac_client* client = user->client; - guac_socket* socket = user->socket; - - /* Draw to buffer */ - guac_layer* cursor = guac_client_alloc_buffer(client); - - cairo_surface_t* graphic = cairo_image_surface_create_for_data( - guac_common_blank_cursor, - guac_common_blank_cursor_format, - guac_common_blank_cursor_width, - guac_common_blank_cursor_height, - guac_common_blank_cursor_stride); - - guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, - 0, 0, graphic); - cairo_surface_destroy(graphic); - - /* Set cursor */ - guac_protocol_send_cursor(socket, 0, 0, cursor, 0, 0, - guac_common_blank_cursor_width, - guac_common_blank_cursor_height); - - /* Free buffer */ - guac_client_free_buffer(client, cursor); - - guac_client_log(client, GUAC_LOG_DEBUG, - "Client cursor image set to generic transparent (blank) cursor."); - -} - diff --git a/src/common/common/blank_cursor.h b/src/common/common/blank_cursor.h deleted file mode 100644 index 86a4a9ff2..000000000 --- a/src/common/common/blank_cursor.h +++ /dev/null @@ -1,64 +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_BLANK_CURSOR_H -#define GUAC_COMMON_BLANK_CURSOR_H - -#include "config.h" - -#include -#include - -/** - * Width of the embedded transparent (blank) mouse cursor graphic. - */ -extern const int guac_common_blank_cursor_width; - -/** - * Height of the embedded transparent (blank) mouse cursor graphic. - */ -extern const int guac_common_blank_cursor_height; - -/** - * Number of bytes in each row of the embedded transparent (blank) mouse cursor - * graphic. - */ -extern const int guac_common_blank_cursor_stride; - -/** - * The Cairo grapic format of the transparent (blank) mouse cursor graphic. - */ -extern const cairo_format_t guac_common_blank_cursor_format; - -/** - * Embedded transparent (blank) mouse cursor graphic. - */ -extern unsigned char guac_common_blank_cursor[]; - -/** - * Sets the cursor of the remote display to the embedded transparent (blank) - * cursor graphic. - * - * @param user - * The guac_user to send the cursor to. - */ -void guac_common_set_blank_cursor(guac_user* user); - -#endif - diff --git a/src/common/common/cursor.h b/src/common/common/cursor.h deleted file mode 100644 index 96c1c6f23..000000000 --- a/src/common/common/cursor.h +++ /dev/null @@ -1,307 +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_CURSOR_H -#define GUAC_COMMON_CURSOR_H - -#include "surface.h" - -#include -#include -#include -#include - -/** - * The default size of the cursor image buffer. - */ -#define GUAC_COMMON_CURSOR_DEFAULT_SIZE 64*64*4 - -/** - * Cursor object which maintains and synchronizes the current mouse cursor - * state across all users of a specific client. - */ -typedef struct guac_common_cursor { - - /** - * The client to maintain the mouse cursor for. - */ - guac_client* client; - - /** - * The buffer containing the current cursor image. - */ - guac_layer* buffer; - - /** - * The width of the cursor image, in pixels. - */ - int width; - - /** - * The height of the cursor image, in pixels. - */ - int height; - - /** - * Arbitrary image data buffer, backing the Cairo surface used to store - * the cursor image. - */ - unsigned char* image_buffer; - - /** - * The size of the image data buffer, in bytes. - */ - size_t image_buffer_size; - - /** - * The current cursor image, if any. If the mouse cursor has not yet been - * set, this will be NULL. - */ - cairo_surface_t* surface; - - /** - * The X coordinate of the hotspot of the mouse cursor. - */ - int hotspot_x; - - /** - * The Y coordinate of the hotspot of the mouse cursor. - */ - int hotspot_y; - - /** - * The last user to move the mouse, or NULL if no user has moved the - * mouse yet. - */ - guac_user* user; - - /** - * The X coordinate of the current mouse cursor location. - */ - int x; - - /** - * The Y coordinate of the current mouse cursor location. - */ - int y; - - /** - * 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 - */ - int button_mask; - - /** - * The server timestamp representing the point in time when the mouse - * location was last updated. - */ - guac_timestamp timestamp; - - /** - * Lock which restricts simultaneous access to the cursor, guaranteeing - * ordered modifications to the cursor and that incompatible operations - * do not occur simultaneously. This lock is for internal use within the - * cursor only. - */ - pthread_mutex_t _lock; - -} guac_common_cursor; - -/** - * Allocates a new cursor object which maintains and synchronizes the current - * mouse cursor state across all users of the given client. - * - * @param client - * The client for which this object shall maintain the mouse cursor. - * - * @return - * The newly-allocated mouse cursor. - */ -guac_common_cursor* guac_common_cursor_alloc(guac_client* client); - -/** - * Frees the given cursor. - * - * @param cursor - * The cursor to free. - */ -void guac_common_cursor_free(guac_common_cursor* cursor); - -/** - * Sends the current state of this cursor across the given socket, including - * the current cursor image. The resulting cursor on the remote display will - * be visible. - * - * @param cursor - * The cursor to send. - * - * @param client - * The user receiving the updated cursor. - * - * @param socket - * The socket over which the updated cursor should be sent. - */ -void guac_common_cursor_dup( - guac_common_cursor* cursor, guac_client* client, guac_socket* socket); - -/** - * Updates the current position and button state of the mouse cursor, marking - * the given user as the most recent user of the mouse. The remote mouse cursor - * will be hidden for this user and shown for all others. - * - * @param cursor - * The cursor being updated. - * - * @param user - * The user that moved the cursor. - * - * @param x - * The new X coordinate of the cursor. - * - * @param y - * The new Y coordinate of the cursor. - * - * @param button_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_common_cursor_update(guac_common_cursor* cursor, guac_user* user, - int x, int y, int button_mask); - -/** - * Sets the cursor image to the given raw image data. This raw image data must - * be in 32-bit ARGB format, having 8 bits per color component, where the - * alpha component is stored in the high-order 8 bits, and blue is stored - * in the low-order 8 bits. - * - * @param cursor - * The cursor to set the image of. - * - * @param hx - * The X coordinate of the hotspot of the new cursor image. - * - * @param hy - * The Y coordinate of the hotspot of the new cursor image. - * - * @param data - * A pointer to raw 32-bit ARGB image data. - * - * @param width - * The width of the given image data, in pixels. - * - * @param height - * The height of the given image data, in pixels. - * - * @param stride - * The number of bytes in a single row of image data. - */ -void guac_common_cursor_set_argb(guac_common_cursor* cursor, int hx, int hy, - unsigned const char* data, int width, int height, int stride); - -/** - * Sets the cursor image to the contents of the given surface. The entire - * contents of the surface are used, and the dimensions of the resulting - * cursor will be the dimensions of the given surface. - * - * @param cursor - * The cursor to set the image of. - * - * @param hx - * The X coordinate of the hotspot of the new cursor image. - * - * @param hy - * The Y coordinate of the hotspot of the new cursor image. - * - * @param surface - * The surface containing the cursor image. - */ -void guac_common_cursor_set_surface(guac_common_cursor* cursor, int hx, int hy, - guac_common_surface* surface); - -/** - * Set the cursor of the remote display to the embedded "pointer" graphic. The - * pointer graphic is a black arrow with white border. - * - * @param cursor - * The cursor to set the image of. - */ -void guac_common_cursor_set_pointer(guac_common_cursor* cursor); - -/** - * Set the cursor of the remote display to the embedded "dot" graphic. The dot - * graphic is a small black square with white border. - * - * @param cursor - * The cursor to set the image of. - */ -void guac_common_cursor_set_dot(guac_common_cursor* cursor); - -/** - * Sets the cursor of the remote display to the embedded "I-bar" graphic. The - * I-bar graphic is a small black "I" shape with white border, used to indicate - * the presence of selectable or editable text. - * - * @param cursor - * The cursor to set the image of. - */ -void guac_common_cursor_set_ibar(guac_common_cursor* cursor); - -/** - * Sets the cursor of the remote display to the embedded transparent (blank) - * graphic, effectively hiding the mouse cursor. - * - * @param cursor - * The cursor to set the image of. - */ -void guac_common_cursor_set_blank(guac_common_cursor* cursor); - -/** - * Removes the given user, such that future synchronization will not occur. - * This is necessary when a user leaves the connection. If a user leaves the - * connection and this is not called, the mouse cursor state may not update - * correctly in response to mouse events. - * - * @param cursor - * The cursor to remove the user from. - * - * @param user - * The user to remove. - */ -void guac_common_cursor_remove_user(guac_common_cursor* cursor, - guac_user* user); - -#endif 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/common/dot_cursor.h b/src/common/common/dot_cursor.h deleted file mode 100644 index c5b73880e..000000000 --- a/src/common/common/dot_cursor.h +++ /dev/null @@ -1,60 +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_DOT_CURSOR_H -#define _GUAC_COMMON_DOT_CURSOR_H - -#include "config.h" - -#include -#include - -/** - * Width of the embedded mouse cursor graphic. - */ -extern const int guac_common_dot_cursor_width; - -/** - * Height of the embedded mouse cursor graphic. - */ -extern const int guac_common_dot_cursor_height; - -/** - * Number of bytes in each row of the embedded mouse cursor graphic. - */ -extern const int guac_common_dot_cursor_stride; - -/** - * The Cairo graphic format of the mouse cursor graphic. - */ -extern const cairo_format_t guac_common_dot_cursor_format; - -/** - * Embedded mouse cursor graphic. - */ -extern unsigned char guac_common_dot_cursor[]; - -/** - * Set the cursor of the remote display to the embedded cursor graphic. - * - * @param user The guac_user to send the cursor to. - */ -void guac_common_set_dot_cursor(guac_user* user); - -#endif diff --git a/src/common/common/ibar_cursor.h b/src/common/common/ibar_cursor.h deleted file mode 100644 index ae11fff7c..000000000 --- a/src/common/common/ibar_cursor.h +++ /dev/null @@ -1,62 +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_IBAR_CURSOR_H -#define GUAC_COMMON_IBAR_CURSOR_H - -#include "config.h" - -#include -#include - -/** - * Width of the embedded I-bar mouse cursor graphic. - */ -extern const int guac_common_ibar_cursor_width; - -/** - * Height of the embedded I-bar mouse cursor graphic. - */ -extern const int guac_common_ibar_cursor_height; - -/** - * Number of bytes in each row of the embedded I-bar mouse cursor graphic. - */ -extern const int guac_common_ibar_cursor_stride; - -/** - * The Cairo grapic format of the I-bar mouse cursor graphic. - */ -extern const cairo_format_t guac_common_ibar_cursor_format; - -/** - * Embedded I-bar mouse cursor graphic. - */ -extern unsigned char guac_common_ibar_cursor[]; - -/** - * Sets the cursor of the remote display to the embedded I-bar cursor graphic. - * - * @param user - * The guac_user to send the cursor to. - */ -void guac_common_set_ibar_cursor(guac_user* user); - -#endif - diff --git a/src/common/common/pointer_cursor.h b/src/common/common/pointer_cursor.h deleted file mode 100644 index 74559ba33..000000000 --- a/src/common/common/pointer_cursor.h +++ /dev/null @@ -1,60 +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_POINTER_CURSOR_H -#define _GUAC_COMMON_POINTER_CURSOR_H - -#include "config.h" - -#include -#include - -/** - * Width of the embedded mouse cursor graphic. - */ -extern const int guac_common_pointer_cursor_width; - -/** - * Height of the embedded mouse cursor graphic. - */ -extern const int guac_common_pointer_cursor_height; - -/** - * Number of bytes in each row of the embedded mouse cursor graphic. - */ -extern const int guac_common_pointer_cursor_stride; - -/** - * The Cairo grapic format of the mouse cursor graphic. - */ -extern const cairo_format_t guac_common_pointer_cursor_format; - -/** - * Embedded mouse cursor graphic. - */ -extern unsigned char guac_common_pointer_cursor[]; - -/** - * Set the cursor of the remote display to the embedded cursor graphic. - * - * @param user The guac_user to send the cursor to. - */ -void guac_common_set_pointer_cursor(guac_user* user); - -#endif diff --git a/src/common/common/rect.h b/src/common/common/rect.h deleted file mode 100644 index 6b3104865..000000000 --- a/src/common/common/rect.h +++ /dev/null @@ -1,143 +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_RECT_H -#define __GUAC_COMMON_RECT_H - -#include "config.h" - -/** - * Simple representation of a rectangle, having a defined corner and dimensions. - */ -typedef struct guac_common_rect { - - /** - * The X coordinate of the upper-left corner of this rectangle. - */ - int x; - - /** - * The Y coordinate of the upper-left corner of this rectangle. - */ - int y; - - /** - * The width of this rectangle. - */ - int width; - - /** - * The height of this rectangle. - */ - int height; - -} guac_common_rect; - -/** - * Initialize the given rect with the given coordinates and dimensions. - * - * @param rect The rect to initialize. - * @param x The X coordinate of the upper-left corner of the rect. - * @param y The Y coordinate of the upper-left corner of the rect. - * @param width The width of the rect. - * @param height The height of the rect. - */ -void guac_common_rect_init(guac_common_rect* rect, int x, int y, int width, int height); - -/** - * Expand the rectangle to fit an NxN grid. - * - * The rectangle will be shifted to the left and up, expanded and adjusted to - * fit within the max bounding rect. - * - * @param cell_size - * The (NxN) grid cell size. - * - * @param rect - * The rectangle to adjust. - * - * @param max_rect - * The bounding area in which the given rect can exist. - * - * @return - * Zero on success, non-zero on error. - */ -int guac_common_rect_expand_to_grid(int cell_size, guac_common_rect* rect, - const guac_common_rect* max_rect); - -/** - * Extend the given rect such that it contains at least the specified minimum - * rect. - * - * @param rect The rect to extend. - * @param min The minimum area which must be contained within the given rect. - */ -void guac_common_rect_extend(guac_common_rect* rect, const guac_common_rect* min); - -/** - * Collapse the given rect such that it exists only within the given maximum - * rect. - * - * @param rect The rect to extend. - * @param max The maximum area in which the given rect can exist. - */ -void guac_common_rect_constrain(guac_common_rect* rect, const guac_common_rect* max); - -/** - * Check whether a rectangle intersects another. - * - * @param rect - * Rectangle to check for intersection. - * - * @param other - * The other rectangle. - * - * @return - * Zero if no intersection, 1 if partial intersection, - * 2 if first rect is completely inside the other. - */ -int guac_common_rect_intersects(const guac_common_rect* rect, - const guac_common_rect* other); - -/** - * Clip and split a rectangle into rectangles which are not covered by the - * hole rectangle. - * - * This function will clip and split single edges when executed and must be - * invoked until it returns zero. The edges are handled counter-clockwise - * starting at the top edge. - * - * @param rect - * The rectangle to be split. This rectangle will be clipped by the - * split_rect. - * - * @param hole - * The rectangle which represents the hole. - * - * @param split_rect - * Resulting split rectangle. - * - * @return - * Zero when no splits were done, non-zero when the rectangle was split. - */ -int guac_common_rect_clip_and_split(guac_common_rect* rect, - const guac_common_rect* hole, guac_common_rect* split_rect); - -#endif - diff --git a/src/common/common/surface.h b/src/common/common/surface.h deleted file mode 100644 index 21859a3e1..000000000 --- a/src/common/common/surface.h +++ /dev/null @@ -1,539 +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_SURFACE_H -#define __GUAC_COMMON_SURFACE_H - -#include "config.h" -#include "rect.h" - -#include -#include -#include -#include -#include - -#include - -/** - * The maximum number of updates to allow within the bitmap queue. - */ -#define GUAC_COMMON_SURFACE_QUEUE_SIZE 256 - -/** - * Heat map cell size in pixels. Each side of each heat map cell will consist - * of this many pixels. - */ -#define GUAC_COMMON_SURFACE_HEAT_CELL_SIZE 64 - -/** - * The width or height of the heat map (in cells) given the width or height of - * the image (in pixels). - */ -#define GUAC_COMMON_SURFACE_HEAT_DIMENSION(x) ( \ - (x + GUAC_COMMON_SURFACE_HEAT_CELL_SIZE - 1) \ - / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE \ -) - -/** - * The number of entries to collect within each heat map cell. Collected - * history entries are used to determine the framerate of the region associated - * with that cell. - */ -#define GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE 5 - -/** - * Representation of a cell in the refresh heat map. This cell is used to keep - * track of how often an area on a surface is refreshed. - */ -typedef struct guac_common_surface_heat_cell { - - /** - * Timestamps of each of the last N updates covering the location - * associated with this heat map cell. This is used to calculate the - * framerate. This array is structured as a ring buffer containing history - * entries in chronologically-ascending order, starting at the entry - * pointed to by oldest_entry and proceeding through all other entries, - * wrapping around if the end of the array is reached. - */ - guac_timestamp history[GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE]; - - /** - * Index of the oldest entry within the history. - */ - int oldest_entry; - -} guac_common_surface_heat_cell; - -/** - * Representation of a bitmap update, having a rectangle of image data (stored - * elsewhere) and a flushed/not-flushed state. - */ -typedef struct guac_common_surface_bitmap_rect { - - /** - * Whether this rectangle has been flushed. - */ - int flushed; - - /** - * The rectangle containing the bitmap update. - */ - guac_common_rect rect; - -} guac_common_surface_bitmap_rect; - -/** - * Surface which backs a Guacamole buffer or layer, automatically - * combining updates when possible. - */ -typedef struct guac_common_surface { - - /** - * The layer this surface will draw to. - */ - const guac_layer* layer; - - /** - * The client associated with this surface. - */ - guac_client* client; - - /** - * The socket to send instructions on when flushing. - */ - guac_socket* socket; - - /** - * 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 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 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 width of this layer, in pixels. - */ - int width; - - /** - * The height of this layer, in pixels. - */ - int height; - - /** - * The size of each image row, in bytes. - */ - int stride; - - /** - * The underlying buffer of the Cairo surface. - */ - unsigned char* buffer; - - /** - * Non-zero if the location or parent layer of this surface has been - * changed and needs to be flushed, 0 otherwise. - */ - int location_dirty; - - /** - * Non-zero if the opacity of this surface has been changed and needs to be - * flushed, 0 otherwise. - */ - int opacity_dirty; - - /** - * Non-zero if this surface is dirty and needs to be flushed, 0 otherwise. - */ - int dirty; - - /** - * The dirty rectangle. - */ - guac_common_rect dirty_rect; - - /** - * Whether the surface actually exists on the client. - */ - int realized; - - /** - * Whether drawing operations are currently clipped by the clipping - * rectangle. - */ - int clipped; - - /** - * The clipping rectangle. - */ - guac_common_rect clip_rect; - - /** - * The number of updates in the bitmap queue. - */ - int bitmap_queue_length; - - /** - * All queued bitmap updates. - */ - guac_common_surface_bitmap_rect bitmap_queue[GUAC_COMMON_SURFACE_QUEUE_SIZE]; - - /** - * A heat map keeping track of the refresh frequency of - * the areas of the screen. - */ - guac_common_surface_heat_cell* heat_map; - - /** - * Mutex which is locked internally when access to the surface must be - * synchronized. All public functions of guac_common_surface should be - * considered threadsafe. - */ - pthread_mutex_t _lock; - -} guac_common_surface; - -/** - * Allocates a new guac_common_surface, assigning it to the given layer. - * - * @param client - * The client associated with the given layer. - * - * @param socket - * The socket to send instructions on when flushing. - * - * @param layer - * The layer to associate with the new surface. - * - * @param w - * The width of the surface. - * - * @param h - * The height of the surface. - * - * @return - * A newly-allocated guac_common_surface. - */ -guac_common_surface* guac_common_surface_alloc(guac_client* client, - guac_socket* socket, const guac_layer* layer, int w, int h); - -/** - * Frees the given guac_common_surface. Beware that this will NOT free any - * associated layers, which must be freed manually. - * - * @param surface The surface to free. - */ -void guac_common_surface_free(guac_common_surface* surface); - - /** - * Resizes the given surface to the given size. - * - * @param surface The surface to resize. - * @param w The width of the surface. - * @param h The height of the surface. - */ -void guac_common_surface_resize(guac_common_surface* surface, int w, int h); - -/** - * Draws the given data to the given guac_common_surface. If the source surface - * is ARGB, the draw operation will be performed using the Porter-Duff "over" - * composite operator. If the source surface is RGB (no alpha channel), no - * compositing is performed and destination pixels are ignored. - * - * @param surface - * The surface to draw to. - * - * @param x - * The X coordinate of the draw location. - * - * @param y - * The Y coordinate of the draw location. - * - * @param src - * The Cairo surface to retrieve data from. - */ -void guac_common_surface_draw(guac_common_surface* surface, int x, int y, - cairo_surface_t* src); - -/** - * Paints to the given guac_common_surface using the given data as a stencil, - * filling opaque regions with the specified color, and leaving transparent - * regions untouched. - * - * @param surface The surface to draw to. - * @param x The X coordinate of the draw location. - * @param y The Y coordinate of the draw location. - * @param src The Cairo surface to retrieve data from. - * @param red The red component of the fill color. - * @param green The green component of the fill color. - * @param blue The blue component of the fill color. - */ -void guac_common_surface_paint(guac_common_surface* surface, int x, int y, cairo_surface_t* src, - int red, int green, int blue); - -/** - * Copies a rectangle of data between two surfaces. - * - * @param src The source surface. - * @param sx The X coordinate of the upper-left corner of the source rect. - * @param sy The Y coordinate of the upper-left corner of the source rect. - * @param w The width of the source rect. - * @param h The height of the source rect. - * @param dst The destination surface. - * @param dx The X coordinate of the upper-left corner of the destination rect. - * @param dy The Y coordinate of the upper-left corner of the destination rect. - */ -void guac_common_surface_copy(guac_common_surface* src, int sx, int sy, int w, int h, - guac_common_surface* dst, int dx, int dy); - -/** - * Transfers a rectangle of data between two surfaces. - * - * @param src The source surface. - * @param sx The X coordinate of the upper-left corner of the source rect. - * @param sy The Y coordinate of the upper-left corner of the source rect. - * @param w The width of the source rect. - * @param h The height of the source rect. - * @param op The transfer function. - * @param dst The destination surface. - * @param dx The X coordinate of the upper-left corner of the destination rect. - * @param dy The Y coordinate of the upper-left corner of the destination rect. - */ -void guac_common_surface_transfer(guac_common_surface* src, int sx, int sy, int w, int h, - guac_transfer_function op, guac_common_surface* dst, int dx, int dy); - -/** - * Assigns the given value to all pixels within a rectangle of the given - * surface. The color of all pixels within the rectangle, including the alpha - * component, is entirely replaced. - * - * @param surface - * The surface to draw upon. - * - * @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 w - * The width of the rectangle. - * - * @param h - * The height of the rectangle. - * - * @param red - * The red component of the color value to assign to all pixels within the - * rectangle. - * - * @param green - * The green component of the color value to assign to all pixels within - * the rectangle. - * - * @param blue - * The blue component of the color value to assign to all pixels within the - * rectangle. - * - * @param alpha - * The alpha component of the color value to assign to all pixels within - * the rectangle. - */ -void guac_common_surface_set(guac_common_surface* surface, int x, int y, - int w, int h, int red, int green, int blue, int alpha); - -/** - * Given the coordinates and dimensions of a rectangle, clips all future - * operations within that rectangle. - * - * @param surface The surface whose clipping rectangle should be changed. - * @param x The X coordinate of the upper-left corner of the bounding rectangle. - * @param y The Y coordinate of the upper-left corner of the bounding rectangle. - * @param w The width of the bounding rectangle. - * @param h The height of the bounding rectangle. - */ -void guac_common_surface_clip(guac_common_surface* surface, int x, int y, int w, int h); - -/** - * Resets the clipping rectangle, allowing drawing operations throughout the - * entire surface. - * - * @param surface The surface whose clipping rectangle should be reset. - */ -void guac_common_surface_reset_clip(guac_common_surface* surface); - -/** - * Changes the location of the surface relative to its parent layer. If the - * surface does not represent a non-default visible layer, then this function - * has no effect. - * - * @param surface - * The surface to move relative to its parent layer. - * - * @param x - * The new X coordinate for the upper-left corner of the layer, in pixels. - * - * @param y - * The new Y coordinate for the upper-left corner of the layer, in pixels. - */ -void guac_common_surface_move(guac_common_surface* surface, int x, int y); - -/** - * Changes the stacking order of the surface relative to other surfaces within - * the same parent layer. If the surface does not represent a non-default - * visible layer, then this function has no effect. - * - * @param surface - * The surface to reorder relative to sibling layers. - * - * @param z - * The new Z-order for this layer, relative to sibling layers. - */ -void guac_common_surface_stack(guac_common_surface* surface, int z); - -/** - * Changes the parent layer of ths given surface. By default, layers will be - * children of the default layer. If the surface does not represent a - * non-default visible layer, then this function has no effect. - * - * @param surface - * The surface whose parent layer should be changed. - * - * @param parent - * The layer which should be set as the new parent of the given surface. - */ -void guac_common_surface_set_parent(guac_common_surface* surface, - const guac_layer* parent); - -/** - * Sets the opacity of the surface. If the surface does not represent a - * non-default visible layer, then this function has no effect. - * - * @param surface - * The surface whose opacity should be changed. - * - * @param opacity - * The level of opacity applied to this surface, where fully opaque is 255, - * and fully transparent is 0. - */ -void guac_common_surface_set_opacity(guac_common_surface* surface, int opacity); - -/** - * Flushes the given surface, including any applicable properties, drawing any - * pending operations on the remote display. - * - * @param surface - * The surface to flush. - */ -void guac_common_surface_flush(guac_common_surface* surface); - -/** - * Duplicates the contents of the current surface to the given socket. Pending - * changes are not flushed. - * - * @param surface - * The surface to duplicate. - * - * @param client - * The client whose users are receiving the surface. - * - * @param socket - * The socket over which the surface contents should be sent. - */ -void guac_common_surface_dup(guac_common_surface* surface, - guac_client* client, guac_socket* socket); - -/** - * Declares that the given surface should receive touch events. By default, - * surfaces are assumed to not expect touch events. This value is advisory, and - * the client is not required to honor the declared level of touch support. - * Implementations are expected to safely handle or ignore any received touch - * events, regardless of the level of touch support declared. regardless of - * the level of touch support declared. - * - * @param surface - * The surface to modify. - * - * @param touches - * The number of simultaneous touches that this surface can accept, where 0 - * indicates that the surface does not support touch events at all. - */ -void guac_common_surface_set_multitouch(guac_common_surface* surface, - int touches); - -/** - * Sets the lossless compression policy of the given surface to the given - * value. By default, newly-created surfaces 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. - * - * @param surface - * The surface to modify. - * - * @param lossless - * Non-zero if all graphical updates for this surface should use lossless - * compression, 0 otherwise. - */ -void guac_common_surface_set_lossless(guac_common_surface* surface, - int lossless); - -#endif - diff --git a/src/common/cursor.c b/src/common/cursor.c deleted file mode 100644 index 9bb51aedf..000000000 --- a/src/common/cursor.c +++ /dev/null @@ -1,327 +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/blank_cursor.h" -#include "common/dot_cursor.h" -#include "common/cursor.h" -#include "common/ibar_cursor.h" -#include "common/pointer_cursor.h" -#include "common/surface.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -/** - * Allocates a cursor as well as an image buffer where the cursor is rendered. - * - * @param client - * The client owning the cursor. - * - * @return - * The newly-allocated cursor or NULL if cursor cannot be allocated. - */ -guac_common_cursor* guac_common_cursor_alloc(guac_client* client) { - - guac_common_cursor* cursor = guac_mem_alloc(sizeof(guac_common_cursor)); - if (cursor == NULL) - return NULL; - - /* Associate cursor with client and allocate cursor buffer */ - cursor->client = client; - cursor->buffer = guac_client_alloc_buffer(client); - - /* Allocate initial image buffer */ - cursor->image_buffer_size = GUAC_COMMON_CURSOR_DEFAULT_SIZE; - cursor->image_buffer = guac_mem_alloc(cursor->image_buffer_size); - - /* No cursor image yet */ - cursor->width = 0; - cursor->height = 0; - cursor->surface = NULL; - cursor->hotspot_x = 0; - cursor->hotspot_y = 0; - - /* No user has moved the mouse yet */ - cursor->user = NULL; - cursor->timestamp = guac_timestamp_current(); - - /* Start cursor in upper-left */ - cursor->x = 0; - cursor->y = 0; - - pthread_mutex_init(&(cursor->_lock), NULL); - - return cursor; - -} - -void guac_common_cursor_free(guac_common_cursor* cursor) { - - pthread_mutex_destroy(&(cursor->_lock)); - - guac_client* client = cursor->client; - guac_layer* buffer = cursor->buffer; - cairo_surface_t* surface = cursor->surface; - - /* Free image buffer and surface */ - guac_mem_free(cursor->image_buffer); - if (surface != NULL) - cairo_surface_destroy(surface); - - /* Destroy buffer within remotely-connected client */ - guac_protocol_send_dispose(client->socket, buffer); - - /* Return buffer to pool */ - guac_client_free_buffer(client, buffer); - - guac_mem_free(cursor); - -} - -void guac_common_cursor_dup( - guac_common_cursor* cursor, guac_client* client, guac_socket* socket) { - - pthread_mutex_lock(&(cursor->_lock)); - - /* Synchronize location */ - guac_protocol_send_mouse(socket, cursor->x, cursor->y, cursor->button_mask, - cursor->timestamp); - - /* Synchronize cursor image */ - if (cursor->surface != NULL) { - guac_protocol_send_size(socket, cursor->buffer, - cursor->width, cursor->height); - - guac_client_stream_png(client, socket, GUAC_COMP_SRC, - cursor->buffer, 0, 0, cursor->surface); - - guac_protocol_send_cursor(socket, - cursor->hotspot_x, cursor->hotspot_y, - cursor->buffer, 0, 0, cursor->width, cursor->height); - } - - pthread_mutex_unlock(&(cursor->_lock)); - - guac_socket_flush(socket); - -} - -/** - * 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_common_cursor whose state should be broadcast to - * all users except the user that moved the cursor last. - * - * @return - * Always NULL. - */ -static void* guac_common_cursor_broadcast_state(guac_user* user, - void* data) { - - guac_common_cursor* cursor = (guac_common_cursor*) data; - - /* Send cursor state only if the user is not moving the cursor */ - if (user != cursor->user) { - guac_protocol_send_mouse(user->socket, cursor->x, cursor->y, - cursor->button_mask, cursor->timestamp); - guac_socket_flush(user->socket); - } - - return NULL; - -} - -void guac_common_cursor_update(guac_common_cursor* cursor, guac_user* user, - int x, int y, int button_mask) { - - pthread_mutex_lock(&(cursor->_lock)); - - /* Update current user of cursor */ - cursor->user = user; - - /* Update cursor state */ - cursor->x = x; - cursor->y = y; - cursor->button_mask = button_mask; - - /* Store time at which cursor was updated */ - cursor->timestamp = guac_timestamp_current(); - - /* Notify all other users of change in cursor state */ - guac_client_foreach_user(cursor->client, - guac_common_cursor_broadcast_state, cursor); - - pthread_mutex_unlock(&(cursor->_lock)); - -} - -/** - * Ensures the cursor image buffer has enough room to fit an image with the - * given characteristics. Existing image buffer data may be destroyed. - * - * @param cursor - * The cursor whose buffer size should be checked. If this cursor lacks - * sufficient space to contain a cursor image of the specified width, - * height, and stride, the current contents of this cursor will be - * destroyed and replaced with an new buffer having sufficient space. - * - * @param width - * The required cursor width, in pixels. - * - * @param height - * The required cursor height, in pixels. - * - * @param stride - * The number of bytes in each row of image data. - */ -static void guac_common_cursor_resize(guac_common_cursor* cursor, - int width, int height, int stride) { - - size_t minimum_size = guac_mem_ckd_mul_or_die(height, stride); - - /* Grow image buffer if necessary */ - if (cursor->image_buffer_size < minimum_size) { - - /* Calculate new size */ - cursor->image_buffer_size = guac_mem_ckd_mul_or_die(minimum_size, 2); - - /* Destructively reallocate image buffer */ - guac_mem_free(cursor->image_buffer); - cursor->image_buffer = guac_mem_alloc(cursor->image_buffer_size); - - } - -} - -void guac_common_cursor_set_argb(guac_common_cursor* cursor, int hx, int hy, - unsigned const char* data, int width, int height, int stride) { - - pthread_mutex_lock(&(cursor->_lock)); - - /* Copy image data */ - guac_common_cursor_resize(cursor, width, height, stride); - memcpy(cursor->image_buffer, data, height * stride); - - if (cursor->surface != NULL) - cairo_surface_destroy(cursor->surface); - - cursor->surface = cairo_image_surface_create_for_data(cursor->image_buffer, - CAIRO_FORMAT_ARGB32, width, height, stride); - - /* Set new cursor parameters */ - cursor->width = width; - cursor->height = height; - cursor->hotspot_x = hx; - cursor->hotspot_y = hy; - - /* Broadcast new cursor image to all users */ - guac_protocol_send_size(cursor->client->socket, cursor->buffer, - width, height); - - guac_client_stream_png(cursor->client, cursor->client->socket, - GUAC_COMP_SRC, cursor->buffer, 0, 0, cursor->surface); - - /* Update cursor image */ - guac_protocol_send_cursor(cursor->client->socket, - cursor->hotspot_x, cursor->hotspot_y, - cursor->buffer, 0, 0, cursor->width, cursor->height); - - guac_socket_flush(cursor->client->socket); - - pthread_mutex_unlock(&(cursor->_lock)); - -} - -void guac_common_cursor_set_surface(guac_common_cursor* cursor, int hx, int hy, - guac_common_surface* surface) { - - /* Set cursor to surface contents */ - guac_common_cursor_set_argb(cursor, hx, hy, surface->buffer, - surface->width, surface->height, surface->stride); - -} - -void guac_common_cursor_set_pointer(guac_common_cursor* cursor) { - - guac_common_cursor_set_argb(cursor, 0, 0, - guac_common_pointer_cursor, - guac_common_pointer_cursor_width, - guac_common_pointer_cursor_height, - guac_common_pointer_cursor_stride); - -} - -void guac_common_cursor_set_dot(guac_common_cursor* cursor) { - - guac_common_cursor_set_argb(cursor, 2, 2, - guac_common_dot_cursor, - guac_common_dot_cursor_width, - guac_common_dot_cursor_height, - guac_common_dot_cursor_stride); - -} - -void guac_common_cursor_set_ibar(guac_common_cursor* cursor) { - - guac_common_cursor_set_argb(cursor, - guac_common_ibar_cursor_width / 2, - guac_common_ibar_cursor_height / 2, - guac_common_ibar_cursor, - guac_common_ibar_cursor_width, - guac_common_ibar_cursor_height, - guac_common_ibar_cursor_stride); - -} - -void guac_common_cursor_set_blank(guac_common_cursor* cursor) { - - guac_common_cursor_set_argb(cursor, 0, 0, - guac_common_blank_cursor, - guac_common_blank_cursor_width, - guac_common_blank_cursor_height, - guac_common_blank_cursor_stride); - -} - -void guac_common_cursor_remove_user(guac_common_cursor* cursor, - guac_user* user) { - - pthread_mutex_lock(&(cursor->_lock)); - - /* Disassociate from given user */ - if (cursor->user == user) - cursor->user = NULL; - - pthread_mutex_unlock(&(cursor->_lock)); - -} - 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/common/dot_cursor.c b/src/common/dot_cursor.c deleted file mode 100644 index fde79b01a..000000000 --- a/src/common/dot_cursor.c +++ /dev/null @@ -1,85 +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 "config.h" - -#include -#include -#include -#include -#include -#include - -/* Macros for prettying up the embedded image. */ -#define X 0x00,0x00,0x00,0xFF -#define O 0xFF,0xFF,0xFF,0xFF -#define _ 0x00,0x00,0x00,0x00 - -/* Dimensions */ -const int guac_common_dot_cursor_width = 5; -const int guac_common_dot_cursor_height = 5; - -/* Format */ -const cairo_format_t guac_common_dot_cursor_format = CAIRO_FORMAT_ARGB32; -const int guac_common_dot_cursor_stride = 20; - -/* Embedded pointer graphic */ -unsigned char guac_common_dot_cursor[] = { - - _,O,O,O,_, - O,X,X,X,O, - O,X,X,X,O, - O,X,X,X,O, - _,O,O,O,_ - -}; - -void guac_common_set_dot_cursor(guac_user* user) { - - guac_client* client = user->client; - guac_socket* socket = user->socket; - - /* Draw to buffer */ - guac_layer* cursor = guac_client_alloc_buffer(client); - - cairo_surface_t* graphic = cairo_image_surface_create_for_data( - guac_common_dot_cursor, - guac_common_dot_cursor_format, - guac_common_dot_cursor_width, - guac_common_dot_cursor_height, - guac_common_dot_cursor_stride); - - guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, - 0, 0, graphic); - cairo_surface_destroy(graphic); - - /* Set cursor */ - guac_protocol_send_cursor(socket, 2, 2, cursor, - 0, 0, - guac_common_dot_cursor_width, - guac_common_dot_cursor_height); - - /* Free buffer */ - guac_client_free_buffer(client, cursor); - - guac_client_log(client, GUAC_LOG_DEBUG, - "Client cursor image set to generic built-in dot."); - -} - diff --git a/src/common/ibar_cursor.c b/src/common/ibar_cursor.c deleted file mode 100644 index 6494591bc..000000000 --- a/src/common/ibar_cursor.c +++ /dev/null @@ -1,98 +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 "config.h" - -#include -#include -#include -#include -#include -#include - -/* Macros for prettying up the embedded image. */ -#define X 0x00,0x00,0x00,0xFF -#define U 0x80,0x80,0x80,0xFF -#define O 0xFF,0xFF,0xFF,0xFF -#define _ 0x00,0x00,0x00,0x00 - -/* Dimensions */ -const int guac_common_ibar_cursor_width = 7; -const int guac_common_ibar_cursor_height = 16; - -/* Format */ -const cairo_format_t guac_common_ibar_cursor_format = CAIRO_FORMAT_ARGB32; -const int guac_common_ibar_cursor_stride = 28; - -/* Embedded I-bar graphic */ -unsigned char guac_common_ibar_cursor[] = { - - 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 - -}; - -void guac_common_set_ibar_cursor(guac_user* user) { - - guac_client* client = user->client; - guac_socket* socket = user->socket; - - /* Draw to buffer */ - guac_layer* cursor = guac_client_alloc_buffer(client); - - cairo_surface_t* graphic = cairo_image_surface_create_for_data( - guac_common_ibar_cursor, - guac_common_ibar_cursor_format, - guac_common_ibar_cursor_width, - guac_common_ibar_cursor_height, - guac_common_ibar_cursor_stride); - - guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, - 0, 0, graphic); - cairo_surface_destroy(graphic); - - /* Set cursor */ - guac_protocol_send_cursor(socket, 0, 0, cursor, - guac_common_ibar_cursor_width / 2, - guac_common_ibar_cursor_height / 2, - guac_common_ibar_cursor_width, - guac_common_ibar_cursor_height); - - /* Free buffer */ - guac_client_free_buffer(client, cursor); - - guac_client_log(client, GUAC_LOG_DEBUG, - "Client cursor image set to generic built-in I-bar."); - -} - diff --git a/src/common/pointer_cursor.c b/src/common/pointer_cursor.c deleted file mode 100644 index fbcb60230..000000000 --- a/src/common/pointer_cursor.c +++ /dev/null @@ -1,96 +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 "config.h" - -#include -#include -#include -#include -#include -#include - -/* Macros for prettying up the embedded image. */ -#define X 0x00,0x00,0x00,0xFF -#define O 0xFF,0xFF,0xFF,0xFF -#define _ 0x00,0x00,0x00,0x00 - -/* Dimensions */ -const int guac_common_pointer_cursor_width = 11; -const int guac_common_pointer_cursor_height = 16; - -/* Format */ -const cairo_format_t guac_common_pointer_cursor_format = CAIRO_FORMAT_ARGB32; -const int guac_common_pointer_cursor_stride = 44; - -/* Embedded pointer graphic */ -unsigned char guac_common_pointer_cursor[] = { - - 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,_,_ - -}; - -void guac_common_set_pointer_cursor(guac_user* user) { - - guac_client* client = user->client; - guac_socket* socket = user->socket; - - /* Draw to buffer */ - guac_layer* cursor = guac_client_alloc_buffer(client); - - cairo_surface_t* graphic = cairo_image_surface_create_for_data( - guac_common_pointer_cursor, - guac_common_pointer_cursor_format, - guac_common_pointer_cursor_width, - guac_common_pointer_cursor_height, - guac_common_pointer_cursor_stride); - - guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, - 0, 0, graphic); - cairo_surface_destroy(graphic); - - /* Set cursor */ - guac_protocol_send_cursor(socket, 0, 0, cursor, - 0, 0, - guac_common_pointer_cursor_width, - guac_common_pointer_cursor_height); - - /* Free buffer */ - guac_client_free_buffer(client, cursor); - - guac_client_log(client, GUAC_LOG_DEBUG, - "Client cursor image set to generic built-in pointer."); - -} - diff --git a/src/common/rect.c b/src/common/rect.c deleted file mode 100644 index a7ca7da52..000000000 --- a/src/common/rect.c +++ /dev/null @@ -1,266 +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 "config.h" -#include "common/rect.h" - -void guac_common_rect_init(guac_common_rect* rect, int x, int y, int width, int height) { - rect->x = x; - rect->y = y; - rect->width = width; - rect->height = height; -} - -void guac_common_rect_extend(guac_common_rect* rect, const guac_common_rect* min) { - - /* Calculate extents of existing dirty rect */ - int left = rect->x; - int top = rect->y; - int right = left + rect->width; - int bottom = top + rect->height; - - /* Calculate missing extents of given new rect */ - int min_left = min->x; - int min_top = min->y; - int min_right = min_left + min->width; - int min_bottom = min_top + min->height; - - /* Update minimums */ - if (min_left < left) left = min_left; - if (min_top < top) top = min_top; - if (min_right > right) right = min_right; - if (min_bottom > bottom) bottom = min_bottom; - - /* Commit rect */ - guac_common_rect_init(rect, left, top, right - left, bottom - top); - -} - -void guac_common_rect_constrain(guac_common_rect* rect, const guac_common_rect* max) { - - /* Calculate extents of existing dirty rect */ - int left = rect->x; - int top = rect->y; - int right = left + rect->width; - int bottom = top + rect->height; - - /* Calculate missing extents of given new rect */ - int max_left = max->x; - int max_top = max->y; - int max_right = max_left + max->width; - int max_bottom = max_top + max->height; - - /* Update maximums */ - if (max_left > left) left = max_left; - if (max_top > top) top = max_top; - if (max_right < right) right = max_right; - if (max_bottom < bottom) bottom = max_bottom; - - /* Commit rect */ - guac_common_rect_init(rect, left, top, right - left, bottom - top); - -} - -int guac_common_rect_expand_to_grid(int cell_size, guac_common_rect* rect, - const guac_common_rect* max_rect) { - - /* Invalid cell_size received */ - if (cell_size <= 0) - return -1; - - /* Nothing to do */ - if (cell_size == 1) - return 0; - - /* Calculate how much the rectangle must be adjusted to fit within the - * given cell size. */ - int dw = cell_size - rect->width % cell_size; - int dh = cell_size - rect->height % cell_size; - - int dx = dw / 2; - int dy = dh / 2; - - /* Set initial extents of adjusted rectangle. */ - int top = rect->y - dy; - int left = rect->x - dx; - int bottom = top + rect->height + dh; - int right = left + rect->width + dw; - - /* The max rectangle */ - int max_left = max_rect->x; - int max_top = max_rect->y; - int max_right = max_left + max_rect->width; - int max_bottom = max_top + max_rect->height; - - /* If the adjusted rectangle has sides beyond the max rectangle, or is larger - * in any direction; shift or adjust the rectangle while trying to fit in - * the grid */ - - /* Adjust left/right */ - if (right > max_right) { - - /* shift to left */ - dw = right - max_right; - right -= dw; - left -= dw; - - /* clamp left if too far */ - if (left < max_left) { - left = max_left; - } - } - else if (left < max_left) { - - /* shift to right */ - dw = max_left - left; - left += dw; - right += dw; - - /* clamp right if too far */ - if (right > max_right) { - right = max_right; - } - } - - /* Adjust top/bottom */ - if (bottom > max_bottom) { - - /* shift up */ - dh = bottom - max_bottom; - bottom -= dh; - top -= dh; - - /* clamp top if too far */ - if (top < max_top) { - top = max_top; - } - } - else if (top < max_top) { - - /* shift down */ - dh = max_top - top; - top += dh; - bottom += dh; - - /* clamp bottom if too far */ - if (bottom > max_bottom) { - bottom = max_bottom; - } - } - - /* Commit rect */ - guac_common_rect_init(rect, left, top, right - left, bottom - top); - - return 0; - -} - -int guac_common_rect_intersects(const guac_common_rect* rect, - const guac_common_rect* other) { - - /* Empty (no intersection) */ - if (other->x + other->width < rect->x || rect->x + rect->width < other->x || - other->y + other->height < rect->y || rect->y + rect->height < other->y) { - return 0; - } - /* Complete */ - else if (other->x <= rect->x && (other->x + other->width) >= (rect->x + rect->width) && - other->y <= rect->y && (other->y + other->height) >= (rect->y + rect->height)) { - return 2; - } - /* Partial intersection */ - return 1; - -} - -int guac_common_rect_clip_and_split(guac_common_rect* rect, - const guac_common_rect* hole, guac_common_rect* split_rect) { - - /* Only continue if the rectangles intersects */ - if (!guac_common_rect_intersects(rect, hole)) - return 0; - - int top, left, bottom, right; - - /* Clip and split top */ - if (rect->y < hole->y) { - top = rect->y; - left = rect->x; - bottom = hole->y; - right = rect->x + rect->width; - guac_common_rect_init(split_rect, left, top, right - left, bottom - top); - - /* Re-initialize original rect */ - top = hole->y; - bottom = rect->y + rect->height; - guac_common_rect_init(rect, left, top, right - left, bottom - top); - - return 1; - } - - /* Clip and split left */ - else if (rect->x < hole->x) { - top = rect->y; - left = rect->x; - bottom = rect->y + rect->height; - right = hole->x; - guac_common_rect_init(split_rect, left, top, right - left, bottom - top); - - /* Re-initialize original rect */ - left = hole->x; - right = rect->x + rect->width; - guac_common_rect_init(rect, left, top, right - left, bottom - top); - - return 1; - } - - /* Clip and split bottom */ - else if (rect->y + rect->height > hole->y + hole->height) { - top = hole->y + hole->height; - left = rect->x; - bottom = rect->y + rect->height; - right = rect->x + rect->width; - guac_common_rect_init(split_rect, left, top, right - left, bottom - top); - - /* Re-initialize original rect */ - top = rect->y; - bottom = hole->y + hole->height; - guac_common_rect_init(rect, left, top, right - left, bottom - top); - - return 1; - } - - /* Clip and split right */ - else if (rect->x + rect->width > hole->x + hole->width) { - top = rect->y; - left = hole->x + hole->width; - bottom = rect->y + rect->height; - right = rect->x + rect->width; - guac_common_rect_init(split_rect, left, top, right - left, bottom - top); - - /* Re-initialize original rect */ - left = rect->x; - right = hole->x + hole->width; - guac_common_rect_init(rect, left, top, right - left, bottom - top); - - return 1; - } - - return 0; -} diff --git a/src/common/surface.c b/src/common/surface.c deleted file mode 100644 index 3ec5517e9..000000000 --- a/src/common/surface.c +++ /dev/null @@ -1,2041 +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 "config.h" -#include "common/rect.h" -#include "common/surface.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -/** - * The width of an update which should be considered negible and thus - * trivial overhead compared ot the cost of two updates. - */ -#define GUAC_SURFACE_NEGLIGIBLE_WIDTH 64 - -/** - * The height of an update which should be considered negible and thus - * trivial overhead compared ot the cost of two updates. - */ -#define GUAC_SURFACE_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_SURFACE_DATA_FACTOR 16 - -/** - * 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_SURFACE_BASE_COST 4096 - -/** - * An increase in cost is negligible if it is less than - * 1/GUAC_SURFACE_NEGLIGIBLE_INCREASE of the old cost. - */ -#define GUAC_SURFACE_NEGLIGIBLE_INCREASE 4 - -/** - * If combining an update because it appears to be follow a fill pattern, - * the combined cost must not exceed - * GUAC_SURFACE_FILL_PATTERN_FACTOR * (total uncombined cost). - */ -#define GUAC_SURFACE_FILL_PATTERN_FACTOR 3 - -/* 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 - -/** - * The framerate which, if exceeded, indicates that JPEG is preferred. - */ -#define GUAC_COMMON_SURFACE_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_SURFACE_JPEG_MIN_BITMAP_SIZE 4096 - -/** - * The JPEG compression min block size. This defines the optimal rectangle block - * size factor for JPEG compression. Usually 8x8 would suffice, but use 16 to - * reduce the occurrence of ringing artifacts further. - */ -#define GUAC_SURFACE_JPEG_BLOCK_SIZE 16 - -/** - * The WebP compression min block size. 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 8 - -void guac_common_surface_set_multitouch(guac_common_surface* surface, - int touches) { - - pthread_mutex_lock(&surface->_lock); - - surface->touches = touches; - guac_protocol_send_set_int(surface->socket, surface->layer, - GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, touches); - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_set_lossless(guac_common_surface* surface, - int lossless) { - - pthread_mutex_lock(&surface->_lock); - surface->lossless = lossless; - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_move(guac_common_surface* surface, int x, int y) { - - pthread_mutex_lock(&surface->_lock); - - surface->x = x; - surface->y = y; - surface->location_dirty = 1; - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_stack(guac_common_surface* surface, int z) { - - pthread_mutex_lock(&surface->_lock); - - surface->z = z; - surface->location_dirty = 1; - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_set_parent(guac_common_surface* surface, - const guac_layer* parent) { - - pthread_mutex_lock(&surface->_lock); - - surface->parent = parent; - surface->location_dirty = 1; - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_set_opacity(guac_common_surface* surface, - int opacity) { - - pthread_mutex_lock(&surface->_lock); - - surface->opacity = opacity; - surface->opacity_dirty = 1; - - pthread_mutex_unlock(&surface->_lock); - -} - -/** - * Updates the coordinates of the given rectangle to be within the bounds of - * the given surface. - * - * @param surface The surface to use for clipping. - * @param rect The rectangle to clip. - * @param sx The X coordinate of the source rectangle, if any. - * @param sy The Y coordinate of the source rectangle, if any. - */ -static void __guac_common_bound_rect(guac_common_surface* surface, - guac_common_rect* rect, int* sx, int* sy) { - - guac_common_rect bounds_rect = { - .x = 0, - .y = 0, - .width = surface->width, - .height = surface->height - }; - - int orig_x = rect->x; - int orig_y = rect->y; - - guac_common_rect_constrain(rect, &bounds_rect); - - /* Update source X/Y if given */ - if (sx != NULL) *sx += rect->x - orig_x; - if (sy != NULL) *sy += rect->y - orig_y; - -} - -/** - * Updates the coordinates of the given rectangle to be within the clipping - * rectangle of the given surface, which must always be within the bounding - * rectangle of the given surface. - * - * @param surface The surface to use for clipping. - * @param rect The rectangle to clip. - * @param sx The X coordinate of the source rectangle, if any. - * @param sy The Y coordinate of the source rectangle, if any. - */ -static void __guac_common_clip_rect(guac_common_surface* surface, - guac_common_rect* rect, int* sx, int* sy) { - - int orig_x = rect->x; - int orig_y = rect->y; - - /* Just bound within surface if no clipping rectangle applied */ - if (!surface->clipped) { - __guac_common_bound_rect(surface, rect, sx, sy); - return; - } - - guac_common_rect_constrain(rect, &surface->clip_rect); - - /* Update source X/Y if given */ - if (sx != NULL) *sx += rect->x - orig_x; - if (sy != NULL) *sy += rect->y - orig_y; - -} - -/** - * Returns whether a rectangle within the given surface contains only fully - * opaque pixels. - * - * @param surface - * The surface to check. - * - * @param rect - * The rectangle to check. - * - * @return - * Non-zero if the rectangle contains only fully opaque pixels, zero - * otherwise. - */ -static int __guac_common_surface_is_opaque(guac_common_surface* surface, - guac_common_rect* rect) { - - int x, y; - - int stride = surface ->stride; - unsigned char* buffer = - surface->buffer + (stride * rect->y) + (4 * rect->x); - - /* For each row */ - for (y = 0; y < rect->height; y++) { - - /* Search for a non-opaque pixel */ - uint32_t* current = (uint32_t*) buffer; - for (x=0; x < rect->width; x++) { - - /* Rectangle is non-opaque if a single non-opaque pixel is found */ - uint32_t color = *(current++); - if ((color & 0xFF000000) != 0xFF000000) - return 0; - - } - - /* Next row */ - buffer += stride; - - } - - /* Rectangle is opaque */ - return 1; - -} - -/** - * Returns whether the given rectangle should be combined into the existing - * dirty rectangle, to be eventually flushed as image data, or would be best - * kept independent of the current rectangle. - * - * @param surface - * The surface being updated. - * - * @param rect - * The bounding rectangle of the update being made to the surface. - * - * @param rect_only - * Non-zero if this update, by its nature, contains only metainformation - * about the update's bounding rectangle, zero if the update also contains - * image data. - * - * @return - * Non-zero if the update should be combined with any existing update, zero - * otherwise. - */ -static int __guac_common_should_combine(guac_common_surface* surface, const guac_common_rect* rect, int rect_only) { - - /* Always favor combining updates if surface is currently a purely - * server-side scratch area */ - if (!surface->realized) - return 1; - - if (surface->dirty) { - - int combined_cost, dirty_cost, update_cost; - - /* Simulate combination */ - guac_common_rect combined = surface->dirty_rect; - guac_common_rect_extend(&combined, rect); - - /* Combine if result is still small */ - if (combined.width <= GUAC_SURFACE_NEGLIGIBLE_WIDTH && combined.height <= GUAC_SURFACE_NEGLIGIBLE_HEIGHT) - return 1; - - /* Estimate costs of the existing update, new update, and both combined */ - combined_cost = GUAC_SURFACE_BASE_COST + combined.width * combined.height; - dirty_cost = GUAC_SURFACE_BASE_COST + surface->dirty_rect.width * surface->dirty_rect.height; - update_cost = GUAC_SURFACE_BASE_COST + rect->width * rect->height; - - /* Reduce cost if no image data */ - if (rect_only) - update_cost /= GUAC_SURFACE_DATA_FACTOR; - - /* Combine if cost estimate shows benefit */ - if (combined_cost <= update_cost + dirty_cost) - return 1; - - /* Combine if increase in cost is negligible */ - if (combined_cost - dirty_cost <= dirty_cost / GUAC_SURFACE_NEGLIGIBLE_INCREASE) - return 1; - - if (combined_cost - update_cost <= update_cost / GUAC_SURFACE_NEGLIGIBLE_INCREASE) - return 1; - - /* Combine if we anticipate further updates, as this update follows a common fill pattern */ - if (rect->x == surface->dirty_rect.x && rect->y == surface->dirty_rect.y + surface->dirty_rect.height) { - if (combined_cost <= (dirty_cost + update_cost) * GUAC_SURFACE_FILL_PATTERN_FACTOR) - return 1; - } - - } - - /* Otherwise, do not combine */ - return 0; - -} - -/** - * Expands the dirty rect of the given surface to contain the rect described by the given - * coordinates. - * - * @param surface The surface to mark as dirty. - * @param rect The rectangle of the update which is dirtying the surface. - */ -static void __guac_common_mark_dirty(guac_common_surface* surface, const guac_common_rect* rect) { - - /* Ignore empty rects */ - if (rect->width <= 0 || rect->height <= 0) - return; - - /* If already dirty, update existing rect */ - if (surface->dirty) - guac_common_rect_extend(&surface->dirty_rect, rect); - - /* Otherwise init dirty rect */ - else { - surface->dirty_rect = *rect; - surface->dirty = 1; - } - -} - -/** - * Calculate the current average framerate for a given area on the surface. - * - * @param surface - * The surface on which the framerate will be calculated. - * - * @param rect - * The rect containing the area for which the average framerate will be - * calculated. - * - * @return - * The average framerate of the given area, in frames per second. - */ -static unsigned int __guac_common_surface_calculate_framerate( - guac_common_surface* surface, const guac_common_rect* rect) { - - int x, y; - - /* Calculate heat map dimensions */ - size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(surface->width); - - /* Calculate minimum X/Y coordinates intersecting given rect */ - int min_x = rect->x / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - int min_y = rect->y / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - - /* Calculate maximum X/Y coordinates intersecting given rect */ - int max_x = min_x + (rect->width - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - int max_y = min_y + (rect->height - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - - unsigned int sum_framerate = 0; - unsigned int count = 0; - - /* Get start of buffer at given coordinates */ - const guac_common_surface_heat_cell* heat_row = - surface->heat_map + min_y * heat_width + min_x; - - /* Iterate over all the heat map cells for the area - * and calculate the average framerate */ - for (y = min_y; y < max_y; y++) { - - /* Get current row of heat map */ - const guac_common_surface_heat_cell* heat_cell = heat_row; - - /* For each cell in subset of row */ - for (x = min_x; x < max_x; x++) { - - /* Calculate indicies for latest and oldest history entries */ - int oldest_entry = heat_cell->oldest_entry; - int latest_entry = oldest_entry - 1; - if (latest_entry < 0) - latest_entry = GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE - 1; - - /* Calculate elapsed time covering entire history for this cell */ - int elapsed_time = heat_cell->history[latest_entry] - - heat_cell->history[oldest_entry]; - - /* Calculate and add framerate */ - if (elapsed_time) - sum_framerate += GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE - * 1000 / elapsed_time; - - /* Next heat map cell */ - heat_cell++; - count++; - - } - - /* Next heat map row */ - heat_row += heat_width; - - } - - /* Calculate the average framerate over entire rect */ - if (count) - return sum_framerate / count; - - return 0; - -} - - /** - * Guesses whether a rectangle within a particular surface 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 surface - * The surface containing the image data to check. - * - * @param rect - * The rect to check within the given surface. - * - * @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 __guac_common_surface_png_optimality(guac_common_surface* surface, - const guac_common_rect* rect) { - - int x, y; - - int num_same = 0; - int num_different = 1; - - /* Get image/buffer metrics */ - int width = rect->width; - int height = rect->height; - int stride = surface->stride; - - /* Get buffer from surface */ - unsigned char* buffer = surface->buffer + rect->y * stride + rect->x * 4; - - /* Image must be at least 1x1 */ - if (width < 1 || height < 1) - return 0; - - /* For each row */ - for (y = 0; y < height; y++) { - - uint32_t* row = (uint32_t*) buffer; - uint32_t last_pixel = *(row++) | 0xFF000000; - - /* For each pixel in current row */ - for (x = 1; x < width; 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 */ - return 0x100 * num_same / num_different - 0x400; - -} - -/** - * Returns whether the given rectangle would be optimally encoded as JPEG - * rather than PNG. - * - * @param surface - * The surface to be queried. - * - * @param rect - * The rectangle to check. - * - * @return - * Non-zero if the rectangle would be optimally encoded as JPEG, zero - * otherwise. - */ -static int __guac_common_surface_should_use_jpeg(guac_common_surface* surface, - const guac_common_rect* rect) { - - /* Do not use JPEG if lossless quality is required */ - if (surface->lossless) - return 0; - - /* Calculate the average framerate for the given rect */ - int framerate = __guac_common_surface_calculate_framerate(surface, rect); - - 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_COMMON_SURFACE_JPEG_FRAMERATE - && rect_size > GUAC_SURFACE_JPEG_MIN_BITMAP_SIZE - && __guac_common_surface_png_optimality(surface, rect) < 0; - -} - -/** - * Returns whether the given rectangle would be optimally encoded as WebP - * rather than PNG. - * - * @param surface - * The surface to be queried. - * - * @param rect - * The rectangle to check. - * - * @return - * Non-zero if the rectangle would be optimally encoded as WebP, zero - * otherwise. - */ -static int __guac_common_surface_should_use_webp(guac_common_surface* surface, - const guac_common_rect* rect) { - - /* Do not use WebP if not supported */ - if (!guac_client_supports_webp(surface->client)) - return 0; - - /* Calculate the average framerate for the given rect */ - int framerate = __guac_common_surface_calculate_framerate(surface, rect); - - /* WebP is preferred if: - * - frame rate is high enough - * - PNG is not more optimal based on image contents */ - return framerate >= GUAC_COMMON_SURFACE_JPEG_FRAMERATE - && __guac_common_surface_png_optimality(surface, rect) < 0; - -} - -/** - * Updates the heat map cells which intersect the given rectangle using the - * given timestamp. This timestamp, along with timestamps from past updates, - * is used to calculate the framerate of each heat cell. - * - * @param surface - * The surface containing the heat map cells to be updated. - * - * @param rect - * The rectangle containing the heat map cells to be updated. - * - * @param time - * The timestamp to use when updating the heat map cells which intersect - * the given rectangle. - */ -static void __guac_common_surface_touch_rect(guac_common_surface* surface, - guac_common_rect* rect, guac_timestamp time) { - - int x, y; - - /* Calculate heat map dimensions */ - size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(surface->width); - - /* Calculate minimum X/Y coordinates intersecting given rect */ - int min_x = rect->x / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - int min_y = rect->y / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - - /* Calculate maximum X/Y coordinates intersecting given rect */ - int max_x = min_x + (rect->width - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - int max_y = min_y + (rect->height - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; - - /* Get start of buffer at given coordinates */ - guac_common_surface_heat_cell* heat_row = - surface->heat_map + min_y * heat_width + min_x; - - /* Update all heat map cells which intersect with rectangle */ - for (y = min_y; y <= max_y; y++) { - - /* Get current row of heat map */ - guac_common_surface_heat_cell* heat_cell = heat_row; - - /* For each cell in subset of row */ - for (x = min_x; x <= max_x; x++) { - - /* Replace oldest entry with new timestamp */ - heat_cell->history[heat_cell->oldest_entry] = time; - - /* Update to next oldest entry */ - heat_cell->oldest_entry++; - if (heat_cell->oldest_entry >= - GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE) - heat_cell->oldest_entry = 0; - - /* Advance to next heat map cell */ - heat_cell++; - - } - - /* Next heat map row */ - heat_row += heat_width; - - } - -} - -/** - * Flushes the bitmap update currently described by the dirty rectangle within the - * given surface to that surface's bitmap queue. There MUST be space within the - * queue. - * - * @param surface The surface to flush. - */ -static void __guac_common_surface_flush_to_queue(guac_common_surface* surface) { - - guac_common_surface_bitmap_rect* rect; - - /* Do not flush if not dirty */ - if (!surface->dirty) - return; - - /* Add new rect to queue */ - rect = &(surface->bitmap_queue[surface->bitmap_queue_length++]); - rect->rect = surface->dirty_rect; - rect->flushed = 0; - - /* Surface now flushed */ - surface->dirty = 0; - -} - -/** - * Flushes the given surface, drawing any pending operations on the remote - * display. Surface properties are not flushed. - * - * @param surface - * The surface to flush. - */ -static void __guac_common_surface_flush(guac_common_surface* surface); - -/** - * Schedules a deferred flush of the given surface. This will not immediately - * flush the surface to the client. Instead, the result of the flush is - * added to a queue which is reinspected and combined (if possible) with other - * deferred flushes during the call to guac_common_surface_flush(). - * - * @param surface The surface to flush. - */ -static void __guac_common_surface_flush_deferred(guac_common_surface* surface) { - - /* Do not flush if not dirty */ - if (!surface->dirty) - return; - - /* Flush if queue size has reached maximum (space is reserved for the final - * dirty rect, as __guac_common_surface_flush() MAY add an additional rect - * to the queue */ - if (surface->bitmap_queue_length == GUAC_COMMON_SURFACE_QUEUE_SIZE-1) - __guac_common_surface_flush(surface); - - /* Append dirty rect to queue */ - __guac_common_surface_flush_to_queue(surface); - -} - -/** - * Transfers a single uint32_t using the given transfer function. - * - * @param op The transfer function to use. - * @param src The source of the uint32_t value. - * @param dst THe destination which will hold the result of the transfer. - * @return Non-zero if the destination value was changed, zero otherwise. - */ -static int __guac_common_surface_transfer_int(guac_transfer_function op, uint32_t* src, uint32_t* dst) { - - uint32_t orig = *dst; - - switch (op) { - - case GUAC_TRANSFER_BINARY_BLACK: - *dst = 0xFF000000; - break; - - case GUAC_TRANSFER_BINARY_WHITE: - *dst = 0xFFFFFFFF; - break; - - case GUAC_TRANSFER_BINARY_SRC: - *dst = *src; - break; - - case GUAC_TRANSFER_BINARY_DEST: - /* NOP */ - break; - - case GUAC_TRANSFER_BINARY_NSRC: - *dst = *src ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_NDEST: - *dst = *dst ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_AND: - *dst = ((*dst) & (0xFF000000 | *src)); - break; - - case GUAC_TRANSFER_BINARY_NAND: - *dst = ((*dst) & (0xFF000000 | *src)) ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_OR: - *dst = ((*dst) | (0x00FFFFFF & *src)); - break; - - case GUAC_TRANSFER_BINARY_NOR: - *dst = ((*dst) | (0x00FFFFFF & *src)) ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_XOR: - *dst = ((*dst) ^ (0x00FFFFFF & *src)); - break; - - case GUAC_TRANSFER_BINARY_XNOR: - *dst = ((*dst) ^ (0x00FFFFFF & *src)) ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_NSRC_AND: - *dst = ((*dst) & (0xFF000000 | (*src ^ 0x00FFFFFF))); - break; - - case GUAC_TRANSFER_BINARY_NSRC_NAND: - *dst = ((*dst) & (0xFF000000 | (*src ^ 0x00FFFFFF))) ^ 0x00FFFFFF; - break; - - case GUAC_TRANSFER_BINARY_NSRC_OR: - *dst = ((*dst) | (0x00FFFFFF & (*src ^ 0x00FFFFFF))); - break; - - case GUAC_TRANSFER_BINARY_NSRC_NOR: - *dst = ((*dst) | (0x00FFFFFF & (*src ^ 0x00FFFFFF))) ^ 0x00FFFFFF; - break; - - } - - return *dst != orig; - -} - -/** - * Assigns the given value to all pixels within a rectangle of the backing - * surface of the given destination surface. The color of all pixels within the - * rectangle, including the alpha component, is entirely replaced. - * - * @param dst - * The destination surface. - * - * @param rect - * The rectangle to draw. - * - * @param red - * The red component of the color value to assign to all pixels within the - * rectangle. - * - * @param green - * The green component of the color value to assign to all pixels within - * the rectangle. - * - * @param blue - * The blue component of the color value to assign to all pixels within the - * rectangle. - * - * @param alpha - * The alpha component of the color value to assign to all pixels within - * the rectangle. - */ -static void __guac_common_surface_set(guac_common_surface* dst, - guac_common_rect* rect, int red, int green, int blue, int alpha) { - - int x, y; - - int dst_stride; - unsigned char* dst_buffer; - - uint32_t color = (alpha << 24) | (red << 16) | (green << 8) | blue; - - int min_x = rect->width - 1; - int min_y = rect->height - 1; - int max_x = 0; - int max_y = 0; - - dst_stride = dst->stride; - dst_buffer = dst->buffer + (dst_stride * rect->y) + (4 * rect->x); - - /* For each row */ - for (y=0; y < rect->height; y++) { - - uint32_t* dst_current = (uint32_t*) dst_buffer; - - /* Set row */ - for (x=0; x < rect->width; x++) { - - uint32_t old_color = *dst_current; - - if (old_color != color) { - if (x < min_x) min_x = x; - if (y < min_y) min_y = y; - if (x > max_x) max_x = x; - if (y > max_y) max_y = y; - *dst_current = color; - } - - dst_current++; - } - - /* Next row */ - dst_buffer += dst_stride; - - } - - /* Restrict destination rect to only updated pixels */ - if (max_x >= min_x && max_y >= min_y) { - rect->x += min_x; - rect->y += min_y; - rect->width = max_x - min_x + 1; - rect->height = max_y - min_y + 1; - } - else { - rect->width = 0; - rect->height = 0; - } - -} - -/** - * Applies the Porter-Duff "over" composite operator, blending the two given - * color components using the given alpha value. - * - * @param dst - * The destination color component. - * - * @param src - * The source color component. - * - * @param alpha - * The alpha value which applies to the blending operation. - * - * @return - * The result of applying the Porter-Duff "over" composite operator to the - * given source and destination components. - */ -static int guac_common_surface_blend_component(int dst, int src, int alpha) { - - int blended = src + dst * (0xFF - alpha); - - /* Do not exceed maximum component value */ - if (blended > 0xFF) - return 0xFF; - - return blended; - -} - -/** - * Applies the Porter-Duff "over" composite operator, blending each component - * of the two given ARGB colors. - * - * @param dst - * The destination ARGB color. - * - * @param src - * The source ARGB color. - * - * @return - * The result of applying the Porter-Duff "over" composite operator to the - * given source and destination colors. - */ -static uint32_t guac_common_surface_argb_blend(uint32_t dst, uint32_t src) { - - /* Separate destination ARGB color into its components */ - int dst_a = (dst >> 24) & 0xFF; - int dst_r = (dst >> 16) & 0xFF; - int dst_g = (dst >> 8) & 0xFF; - int dst_b = dst & 0xFF; - - /* Separate source ARGB color into its components */ - int src_a = (src >> 24) & 0xFF; - int src_r = (src >> 16) & 0xFF; - int src_g = (src >> 8) & 0xFF; - int src_b = src & 0xFF; - - /* If source is fully opaque (or destination is fully transparent), the - * blended result is the source */ - if (src_a == 0xFF || dst_a == 0x00) - return src; - - /* If source is fully transparent, the blended result is the destination */ - if (src_a == 0x00) - return dst; - - /* Otherwise, blend each ARGB component, assuming pre-multiplied alpha */ - int r = guac_common_surface_blend_component(dst_r, src_r, src_a); - int g = guac_common_surface_blend_component(dst_g, src_g, src_a); - int b = guac_common_surface_blend_component(dst_b, src_b, src_a); - int a = guac_common_surface_blend_component(dst_a, src_a, src_a); - - /* Recombine blended components */ - return (a << 24) | (r << 16) | (g << 8) | b; - -} - -/** - * Copies data from the given buffer to the surface at the given coordinates. - * The dimensions and location of the destination rectangle will be altered - * to remove as many unchanged pixels as possible. - * - * @param src_buffer The buffer to copy. - * @param src_stride The number of bytes in each row of the source buffer. - * @param sx The X coordinate of the source rectangle. - * @param sy The Y coordinate of the source rectangle. - * @param dst The destination surface. - * @param rect The destination rectangle. - * @param opaque Non-zero if the source surface is opaque (its alpha channel - * should be ignored), zero otherwise. - */ -static void __guac_common_surface_put(unsigned char* src_buffer, int src_stride, - int* sx, int* sy, - guac_common_surface* dst, guac_common_rect* rect, - int opaque) { - - unsigned char* dst_buffer = dst->buffer; - int dst_stride = dst->stride; - - int x, y; - - int min_x = rect->width; - int min_y = rect->height; - int max_x = 0; - int max_y = 0; - - int orig_x = rect->x; - int orig_y = rect->y; - - src_buffer += src_stride * (*sy) + 4 * (*sx); - dst_buffer += (dst_stride * rect->y) + (4 * rect->x); - - /* For each row */ - for (y=0; y < rect->height; y++) { - - uint32_t* src_current = (uint32_t*) src_buffer; - uint32_t* dst_current = (uint32_t*) dst_buffer; - - /* Copy row */ - for (x=0; x < rect->width; x++) { - - uint32_t color; - - /* Get source and destination color values */ - uint32_t src_color = *src_current; - uint32_t dst_color = *dst_current; - - /* Ignore alpha channel if opaque */ - if (opaque) - color = src_color | 0xFF000000; - - /* Otherwise, perform alpha blending operation */ - else - color = guac_common_surface_argb_blend(dst_color, src_color); - - /* If the destination color is changing, update rectangle bounds - * and store the new color */ - if (dst_color != color) { - if (x < min_x) min_x = x; - if (y < min_y) min_y = y; - if (x > max_x) max_x = x; - if (y > max_y) max_y = y; - *dst_current = color; - } - - /* Advance to next pixel */ - src_current++; - dst_current++; - - } - - /* Next row */ - src_buffer += src_stride; - dst_buffer += dst_stride; - - } - - /* Restrict destination rect to only updated pixels */ - if (max_x >= min_x && max_y >= min_y) { - rect->x += min_x; - rect->y += min_y; - rect->width = max_x - min_x + 1; - rect->height = max_y - min_y + 1; - } - else { - rect->width = 0; - rect->height = 0; - } - - /* Update source X/Y */ - *sx += rect->x - orig_x; - *sy += rect->y - orig_y; - -} - -/** - * Fills the given surface with color, using the given buffer as a mask. Color - * will be added to the given surface iff the corresponding pixel within the - * buffer is opaque. - * - * @param src_buffer The buffer to use as a mask. - * @param src_stride The number of bytes in each row of the source buffer. - * @param sx The X coordinate of the source rectangle. - * @param sy The Y coordinate of the source rectangle. - * @param dst The destination surface. - * @param rect The destination rectangle. - * @param red The red component of the color of the fill. - * @param green The green component of the color of the fill. - * @param blue The blue component of the color of the fill. - */ -static void __guac_common_surface_fill_mask(unsigned char* src_buffer, int src_stride, - int sx, int sy, - guac_common_surface* dst, guac_common_rect* rect, - int red, int green, int blue) { - - unsigned char* dst_buffer = dst->buffer; - int dst_stride = dst->stride; - - uint32_t color = 0xFF000000 | (red << 16) | (green << 8) | blue; - int x, y; - - src_buffer += src_stride*sy + 4*sx; - dst_buffer += (dst_stride * rect->y) + (4 * rect->x); - - /* For each row */ - for (y=0; y < rect->height; y++) { - - uint32_t* src_current = (uint32_t*) src_buffer; - uint32_t* dst_current = (uint32_t*) dst_buffer; - - /* Stencil row */ - for (x=0; x < rect->width; x++) { - - /* Fill with color if opaque */ - if (*src_current & 0xFF000000) - *dst_current = color; - - src_current++; - dst_current++; - } - - /* Next row */ - src_buffer += src_stride; - dst_buffer += dst_stride; - - } - -} - -/** - * Copies data from the given surface to the given destination surface using - * the specified transfer function. - * - * @param src_buffer The buffer to copy. - * @param src_stride The number of bytes in each row of the source buffer. - * @param sx The X coordinate of the source rectangle. - * @param sy The Y coordinate of the source rectangle. - * @param op The transfer function to use. - * @param dst The destination surface. - * @param rect The destination rectangle. - */ -static void __guac_common_surface_transfer(guac_common_surface* src, int* sx, int* sy, - guac_transfer_function op, - guac_common_surface* dst, guac_common_rect* rect) { - - unsigned char* src_buffer = src->buffer; - unsigned char* dst_buffer = dst->buffer; - - int x, y; - int src_stride, dst_stride; - int step = 1; - - int min_x = rect->width - 1; - int min_y = rect->height - 1; - int max_x = 0; - int max_y = 0; - - int orig_x = rect->x; - int orig_y = rect->y; - - /* Copy forwards only if destination is in a different surface or is before source */ - if (src != dst || rect->y < *sy || (rect->y == *sy && rect->x < *sx)) { - src_buffer += src->stride * (*sy) + 4 * (*sx); - dst_buffer += (dst->stride * rect->y) + (4 * rect->x); - src_stride = src->stride; - dst_stride = dst->stride; - step = 1; - } - - /* Otherwise, copy backwards */ - else { - src_buffer += src->stride * (*sy + rect->height - 1) + 4 * (*sx + rect->width - 1); - dst_buffer += dst->stride * (rect->y + rect->height - 1) + 4 * (rect->x + rect->width - 1); - src_stride = -src->stride; - dst_stride = -dst->stride; - step = -1; - } - - /* For each row */ - for (y=0; y < rect->height; y++) { - - uint32_t* src_current = (uint32_t*) src_buffer; - uint32_t* dst_current = (uint32_t*) dst_buffer; - - /* Transfer each pixel in row */ - for (x=0; x < rect->width; x++) { - - if (__guac_common_surface_transfer_int(op, src_current, dst_current)) { - if (x < min_x) min_x = x; - if (y < min_y) min_y = y; - if (x > max_x) max_x = x; - if (y > max_y) max_y = y; - } - - src_current += step; - dst_current += step; - } - - /* Next row */ - src_buffer += src_stride; - dst_buffer += dst_stride; - - } - - /* Translate X coordinate space of moving backwards */ - if (step < 0) { - int old_max_x = max_x; - max_x = rect->width - 1 - min_x; - min_x = rect->width - 1 - old_max_x; - } - - /* Translate Y coordinate space of moving backwards */ - if (dst_stride < 0) { - int old_max_y = max_y; - max_y = rect->height - 1 - min_y; - min_y = rect->height - 1 - old_max_y; - } - - /* Restrict destination rect to only updated pixels */ - if (max_x >= min_x && max_y >= min_y) { - rect->x += min_x; - rect->y += min_y; - rect->width = max_x - min_x + 1; - rect->height = max_y - min_y + 1; - } - else { - rect->width = 0; - rect->height = 0; - } - - /* Update source X/Y */ - *sx += rect->x - orig_x; - *sy += rect->y - orig_y; - -} - -guac_common_surface* guac_common_surface_alloc(guac_client* client, - guac_socket* socket, const guac_layer* layer, int w, int h) { - - /* Calculate heat map dimensions */ - size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(w); - size_t heat_height = GUAC_COMMON_SURFACE_HEAT_DIMENSION(h); - - /* Init surface */ - guac_common_surface* surface = guac_mem_zalloc(sizeof(guac_common_surface)); - surface->client = client; - surface->socket = socket; - surface->layer = layer; - surface->parent = GUAC_DEFAULT_LAYER; - surface->opacity = 0xFF; - surface->width = w; - surface->height = h; - - pthread_mutex_init(&surface->_lock, NULL); - - /* Create corresponding Cairo surface */ - surface->stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); - surface->buffer = guac_mem_zalloc(h, surface->stride); - - /* Create corresponding heat map */ - surface->heat_map = guac_mem_zalloc(heat_width, heat_height, - sizeof(guac_common_surface_heat_cell)); - - /* Reset clipping rect */ - guac_common_surface_reset_clip(surface); - - /* Layers must initially exist */ - if (layer->index >= 0) { - guac_protocol_send_size(socket, layer, w, h); - surface->realized = 1; - } - - /* Defer creation of buffers */ - else - surface->realized = 0; - - return surface; -} - -void guac_common_surface_free(guac_common_surface* surface) { - - /* Only dispose of surface if it exists */ - if (surface->realized) - guac_protocol_send_dispose(surface->socket, surface->layer); - - pthread_mutex_destroy(&surface->_lock); - - guac_mem_free(surface->heat_map); - guac_mem_free(surface->buffer); - guac_mem_free(surface); - -} - -void guac_common_surface_resize(guac_common_surface* surface, int w, int h) { - - pthread_mutex_lock(&surface->_lock); - - /* Ignore if resize will have no effect */ - if (w == surface->width && h == surface->height) - goto complete; - - guac_socket* socket = surface->socket; - const guac_layer* layer = surface->layer; - - unsigned char* old_buffer; - int old_stride; - guac_common_rect old_rect; - - int sx = 0; - int sy = 0; - - /* Calculate heat map dimensions */ - size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(w); - size_t heat_height = GUAC_COMMON_SURFACE_HEAT_DIMENSION(h); - - /* Copy old surface data */ - old_buffer = surface->buffer; - old_stride = surface->stride; - guac_common_rect_init(&old_rect, 0, 0, surface->width, surface->height); - - /* Re-initialize at new size */ - surface->width = w; - surface->height = h; - surface->stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); - surface->buffer = guac_mem_zalloc(h, surface->stride); - __guac_common_bound_rect(surface, &surface->clip_rect, NULL, NULL); - - /* Copy relevant old data */ - __guac_common_bound_rect(surface, &old_rect, NULL, NULL); - __guac_common_surface_put(old_buffer, old_stride, &sx, &sy, surface, &old_rect, 1); - - /* Free old data */ - guac_mem_free(old_buffer); - - /* Allocate completely new heat map (can safely discard old stats) */ - guac_mem_free(surface->heat_map); - surface->heat_map = guac_mem_zalloc(heat_width, heat_height, - sizeof(guac_common_surface_heat_cell)); - - /* Resize dirty rect to fit new surface dimensions */ - if (surface->dirty) { - __guac_common_bound_rect(surface, &surface->dirty_rect, NULL, NULL); - if (surface->dirty_rect.width <= 0 || surface->dirty_rect.height <= 0) - surface->dirty = 0; - } - - /* Update Guacamole layer */ - if (surface->realized) - guac_protocol_send_size(socket, layer, w, h); - -complete: - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_draw(guac_common_surface* surface, int x, int y, cairo_surface_t* src) { - - pthread_mutex_lock(&surface->_lock); - - unsigned char* buffer = cairo_image_surface_get_data(src); - cairo_format_t format = cairo_image_surface_get_format(src); - int stride = cairo_image_surface_get_stride(src); - int w = cairo_image_surface_get_width(src); - int h = cairo_image_surface_get_height(src); - - int sx = 0; - int sy = 0; - - guac_common_rect rect; - guac_common_rect_init(&rect, x, y, w, h); - - /* Clip operation */ - __guac_common_clip_rect(surface, &rect, &sx, &sy); - if (rect.width <= 0 || rect.height <= 0) - goto complete; - - /* Update backing surface */ - __guac_common_surface_put(buffer, stride, &sx, &sy, surface, &rect, format != CAIRO_FORMAT_ARGB32); - if (rect.width <= 0 || rect.height <= 0) - goto complete; - - /* Update the heat map for the update rectangle. */ - guac_timestamp time = guac_timestamp_current(); - __guac_common_surface_touch_rect(surface, &rect, time); - - /* Flush if not combining */ - if (!__guac_common_should_combine(surface, &rect, 0)) - __guac_common_surface_flush_deferred(surface); - - /* Always defer draws */ - __guac_common_mark_dirty(surface, &rect); - -complete: - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_paint(guac_common_surface* surface, int x, int y, - cairo_surface_t* src, int red, int green, int blue) { - - pthread_mutex_lock(&surface->_lock); - - unsigned char* buffer = cairo_image_surface_get_data(src); - int stride = cairo_image_surface_get_stride(src); - int w = cairo_image_surface_get_width(src); - int h = cairo_image_surface_get_height(src); - - int sx = 0; - int sy = 0; - - guac_common_rect rect; - guac_common_rect_init(&rect, x, y, w, h); - - /* Clip operation */ - __guac_common_clip_rect(surface, &rect, &sx, &sy); - if (rect.width <= 0 || rect.height <= 0) - goto complete; - - /* Update backing surface */ - __guac_common_surface_fill_mask(buffer, stride, sx, sy, surface, &rect, red, green, blue); - - /* Flush if not combining */ - if (!__guac_common_should_combine(surface, &rect, 0)) - __guac_common_surface_flush_deferred(surface); - - /* Always defer draws */ - __guac_common_mark_dirty(surface, &rect); - -complete: - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_copy(guac_common_surface* src, int sx, int sy, - int w, int h, guac_common_surface* dst, int dx, int dy) { - - /* Lock both surfaces */ - pthread_mutex_lock(&dst->_lock); - if (src != dst) - pthread_mutex_lock(&src->_lock); - - guac_socket* socket = dst->socket; - const guac_layer* src_layer = src->layer; - const guac_layer* dst_layer = dst->layer; - - guac_common_rect srect; - guac_common_rect_init(&srect, sx, sy, w, h); - - /* Clip operation source rect to bounds */ - __guac_common_bound_rect(src, &srect, &dx, &dy); - if (srect.width <= 0 || srect.height <= 0) - goto complete; - - guac_common_rect drect; - guac_common_rect_init(&drect, dx, dy, - srect.width, srect.height); - - /* Clip operation destination rect */ - __guac_common_clip_rect(dst, &drect, &srect.x, &srect.y); - if (drect.width <= 0 || drect.height <= 0) - goto complete; - - /* NOTE: Being the last rectangle to be adjusted, only the width/height of - * drect is now correct! */ - - /* Update backing surface first only if drect cannot intersect srect */ - if (src != dst) { - __guac_common_surface_transfer(src, &srect.x, &srect.y, - GUAC_TRANSFER_BINARY_SRC, dst, &drect); - if (drect.width <= 0 || drect.height <= 0) - goto complete; - } - - /* Defer if combining */ - if (__guac_common_should_combine(dst, &drect, 1)) - __guac_common_mark_dirty(dst, &drect); - - /* Otherwise, flush and draw immediately */ - else { - __guac_common_surface_flush(dst); - __guac_common_surface_flush(src); - guac_protocol_send_copy(socket, src_layer, srect.x, srect.y, - drect.width, drect.height, GUAC_COMP_OVER, dst_layer, - drect.x, drect.y); - dst->realized = 1; - } - - /* Update backing surface last if drect can intersect srect */ - if (src == dst) - __guac_common_surface_transfer(src, &srect.x, &srect.y, - GUAC_TRANSFER_BINARY_SRC, dst, &drect); - -complete: - - /* Unlock both surfaces */ - pthread_mutex_unlock(&dst->_lock); - if (src != dst) - pthread_mutex_unlock(&src->_lock); - -} - -void guac_common_surface_transfer(guac_common_surface* src, int sx, int sy, int w, int h, - guac_transfer_function op, guac_common_surface* dst, int dx, int dy) { - - /* Lock both surfaces */ - pthread_mutex_lock(&dst->_lock); - if (src != dst) - pthread_mutex_lock(&src->_lock); - - guac_socket* socket = dst->socket; - const guac_layer* src_layer = src->layer; - const guac_layer* dst_layer = dst->layer; - - guac_common_rect srect; - guac_common_rect_init(&srect, sx, sy, w, h); - - /* Clip operation source rect to bounds */ - __guac_common_bound_rect(src, &srect, &dx, &dy); - if (srect.width <= 0 || srect.height <= 0) - goto complete; - - guac_common_rect drect; - guac_common_rect_init(&drect, dx, dy, - srect.width, srect.height); - - /* Clip operation destination rect */ - __guac_common_clip_rect(dst, &drect, &srect.x, &srect.y); - if (drect.width <= 0 || drect.height <= 0) - goto complete; - - /* NOTE: Being the last rectangle to be adjusted, only the width/height of - * drect is now correct! */ - - /* Update backing surface first only if drect cannot intersect srect */ - if (src != dst) { - __guac_common_surface_transfer(src, &srect.x, &srect.y, op, dst, &drect); - if (drect.width <= 0 || drect.height <= 0) - goto complete; - } - - /* Defer if combining */ - if (__guac_common_should_combine(dst, &drect, 1)) - __guac_common_mark_dirty(dst, &drect); - - /* Otherwise, flush and draw immediately */ - else { - __guac_common_surface_flush(dst); - __guac_common_surface_flush(src); - guac_protocol_send_transfer(socket, src_layer, srect.x, srect.y, - drect.width, drect.height, op, dst_layer, drect.x, drect.y); - dst->realized = 1; - } - - /* Update backing surface last if drect can intersect srect */ - if (src == dst) - __guac_common_surface_transfer(src, &srect.x, &srect.y, op, dst, &drect); - -complete: - - /* Unlock both surfaces */ - pthread_mutex_unlock(&dst->_lock); - if (src != dst) - pthread_mutex_unlock(&src->_lock); - -} - -void guac_common_surface_set(guac_common_surface* surface, - int x, int y, int w, int h, int red, int green, int blue, int alpha) { - - pthread_mutex_lock(&surface->_lock); - - guac_socket* socket = surface->socket; - const guac_layer* layer = surface->layer; - - guac_common_rect rect; - guac_common_rect_init(&rect, x, y, w, h); - - /* Clip operation */ - __guac_common_clip_rect(surface, &rect, NULL, NULL); - if (rect.width <= 0 || rect.height <= 0) - goto complete; - - /* Update backing surface */ - __guac_common_surface_set(surface, &rect, red, green, blue, alpha); - if (rect.width <= 0 || rect.height <= 0) - goto complete; - - /* Handle as normal draw if non-opaque */ - if (alpha != 0xFF) { - - /* Flush if not combining */ - if (!__guac_common_should_combine(surface, &rect, 0)) - __guac_common_surface_flush_deferred(surface); - - /* Always defer draws */ - __guac_common_mark_dirty(surface, &rect); - - } - - /* Defer if combining */ - else if (__guac_common_should_combine(surface, &rect, 1)) - __guac_common_mark_dirty(surface, &rect); - - /* Otherwise, flush and draw immediately */ - else { - __guac_common_surface_flush(surface); - guac_protocol_send_rect(socket, layer, rect.x, rect.y, rect.width, rect.height); - guac_protocol_send_cfill(socket, GUAC_COMP_OVER, layer, red, green, blue, alpha); - surface->realized = 1; - } - -complete: - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_clip(guac_common_surface* surface, int x, int y, int w, int h) { - - pthread_mutex_lock(&surface->_lock); - - guac_common_rect clip; - - /* Init clipping rectangle if clipping not already applied */ - if (!surface->clipped) { - guac_common_rect_init(&surface->clip_rect, 0, 0, surface->width, surface->height); - surface->clipped = 1; - } - - guac_common_rect_init(&clip, x, y, w, h); - guac_common_rect_constrain(&surface->clip_rect, &clip); - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_reset_clip(guac_common_surface* surface) { - pthread_mutex_lock(&surface->_lock); - surface->clipped = 0; - pthread_mutex_unlock(&surface->_lock); -} - -/** - * Flushes the bitmap update currently described by the dirty rectangle within - * the given surface directly via an "img" instruction as PNG data. The - * resulting instructions will be sent over the socket associated with the - * given surface. - * - * @param surface - * The surface to flush. - * - * @param opaque - * Whether the rectangle being flushed contains only fully-opaque pixels. - */ -static void __guac_common_surface_flush_to_png(guac_common_surface* surface, - int opaque) { - - if (surface->dirty) { - - guac_socket* socket = surface->socket; - const guac_layer* layer = surface->layer; - - /* Get Cairo surface for specified rect */ - unsigned char* buffer = surface->buffer - + surface->dirty_rect.y * surface->stride - + surface->dirty_rect.x * 4; - - cairo_surface_t* rect; - - /* Use RGB24 if the image is fully opaque */ - if (opaque) - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_RGB24, surface->dirty_rect.width, - surface->dirty_rect.height, surface->stride); - - /* Otherwise ARGB32 is needed */ - else { - - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_ARGB32, surface->dirty_rect.width, - surface->dirty_rect.height, surface->stride); - - /* Clear destination rect first */ - guac_protocol_send_rect(socket, layer, - surface->dirty_rect.x, surface->dirty_rect.y, - surface->dirty_rect.width, surface->dirty_rect.height); - guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, - 0x00, 0x00, 0x00, 0xFF); - - } - - /* Send PNG for rect */ - guac_client_stream_png(surface->client, socket, GUAC_COMP_OVER, - layer, surface->dirty_rect.x, surface->dirty_rect.y, rect); - - cairo_surface_destroy(rect); - surface->realized = 1; - - /* Surface is no longer dirty */ - surface->dirty = 0; - - } - -} - -/** - * 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_common_surface_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; - -} - -/** - * Flushes the bitmap update currently described by the dirty rectangle within - * the given surface directly via an "img" instruction as JPEG data. The - * resulting instructions will be sent over the socket associated with the - * given surface. - * - * @param surface - * The surface to flush. - */ -static void __guac_common_surface_flush_to_jpeg(guac_common_surface* surface) { - - if (surface->dirty) { - - guac_socket* socket = surface->socket; - const guac_layer* layer = surface->layer; - - guac_common_rect max; - guac_common_rect_init(&max, 0, 0, surface->width, surface->height); - - /* Expand the dirty rect size to fit in a grid with cells equal to the - * minimum JPEG block size */ - guac_common_rect_expand_to_grid(GUAC_SURFACE_JPEG_BLOCK_SIZE, - &surface->dirty_rect, &max); - - /* Get Cairo surface for specified rect */ - unsigned char* buffer = surface->buffer - + surface->dirty_rect.y * surface->stride - + surface->dirty_rect.x * 4; - - cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_RGB24, surface->dirty_rect.width, - surface->dirty_rect.height, surface->stride); - - /* Send JPEG for rect */ - guac_client_stream_jpeg(surface->client, socket, GUAC_COMP_OVER, layer, - surface->dirty_rect.x, surface->dirty_rect.y, rect, - guac_common_surface_suggest_quality(surface->client)); - - cairo_surface_destroy(rect); - surface->realized = 1; - - /* Surface is no longer dirty */ - surface->dirty = 0; - - } - -} - -/** - * Flushes the bitmap update currently described by the dirty rectangle within - * the given surface directly via an "img" instruction as WebP data. The - * resulting instructions will be sent over the socket associated with the - * given surface. - * - * @param surface - * The surface to flush. - * - * @param opaque - * Whether the rectangle being flushed contains only fully-opaque pixels. - */ -static void __guac_common_surface_flush_to_webp(guac_common_surface* surface, - int opaque) { - - if (surface->dirty) { - - guac_socket* socket = surface->socket; - const guac_layer* layer = surface->layer; - - guac_common_rect max; - guac_common_rect_init(&max, 0, 0, surface->width, surface->height); - - /* Expand the dirty rect size to fit in a grid with cells equal to the - * minimum WebP block size */ - guac_common_rect_expand_to_grid(GUAC_SURFACE_WEBP_BLOCK_SIZE, - &surface->dirty_rect, &max); - - /* Get Cairo surface for specified rect */ - unsigned char* buffer = surface->buffer - + surface->dirty_rect.y * surface->stride - + surface->dirty_rect.x * 4; - - cairo_surface_t* rect; - - /* Use RGB24 if the image is fully opaque */ - if (opaque) - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_RGB24, surface->dirty_rect.width, - surface->dirty_rect.height, surface->stride); - - /* Otherwise ARGB32 is needed */ - else - rect = cairo_image_surface_create_for_data(buffer, - CAIRO_FORMAT_ARGB32, surface->dirty_rect.width, - surface->dirty_rect.height, surface->stride); - - /* Send WebP for rect */ - guac_client_stream_webp(surface->client, socket, GUAC_COMP_OVER, layer, - surface->dirty_rect.x, surface->dirty_rect.y, rect, - guac_common_surface_suggest_quality(surface->client), - surface->lossless ? 1 : 0); - - cairo_surface_destroy(rect); - surface->realized = 1; - - /* Surface is no longer dirty */ - surface->dirty = 0; - - } - -} - -/** - * Comparator for instances of guac_common_surface_bitmap_rect, the elements - * which make up a surface's bitmap buffer. - * - * @see qsort - */ -static int __guac_common_surface_bitmap_rect_compare(const void* a, const void* b) { - - guac_common_surface_bitmap_rect* ra = (guac_common_surface_bitmap_rect*) a; - guac_common_surface_bitmap_rect* rb = (guac_common_surface_bitmap_rect*) b; - - /* Order roughly top to bottom, left to right */ - if (ra->rect.y != rb->rect.y) return ra->rect.y - rb->rect.y; - if (ra->rect.x != rb->rect.x) return ra->rect.x - rb->rect.x; - - /* Wider updates should come first (more likely to intersect later) */ - if (ra->rect.width != rb->rect.width) return rb->rect.width - ra->rect.width; - - /* Shorter updates should come first (less likely to increase cost) */ - return ra->rect.height - rb->rect.height; - -} - -/** - * Flushes only the properties of the given surface, such as layer location or - * opacity. Image state is not flushed. If the surface represents a buffer or - * the default layer, this function has no effect. - * - * @param surface - * The surface to flush. - */ -static void __guac_common_surface_flush_properties( - guac_common_surface* surface) { - - guac_socket* socket = surface->socket; - - /* Only applicable to non-default visible layers */ - if (surface->layer->index <= 0) - return; - - /* Flush opacity */ - if (surface->opacity_dirty) { - guac_protocol_send_shade(socket, surface->layer, surface->opacity); - surface->opacity_dirty = 0; - } - - /* Flush location and hierarchy */ - if (surface->location_dirty) { - guac_protocol_send_move(socket, surface->layer, - surface->parent, surface->x, surface->y, surface->z); - surface->location_dirty = 0; - } - -} - -static void __guac_common_surface_flush(guac_common_surface* surface) { - - /* Flush final dirty rectangle to queue. */ - __guac_common_surface_flush_to_queue(surface); - - guac_common_surface_bitmap_rect* current = surface->bitmap_queue; - int i, j; - int original_queue_length; - int flushed = 0; - - original_queue_length = surface->bitmap_queue_length; - - /* Sort updates to make combination less costly */ - qsort(surface->bitmap_queue, surface->bitmap_queue_length, sizeof(guac_common_surface_bitmap_rect), - __guac_common_surface_bitmap_rect_compare); - - /* Flush all rects in queue */ - for (i=0; i < surface->bitmap_queue_length; i++) { - - /* Get next unflushed candidate */ - guac_common_surface_bitmap_rect* candidate = current; - if (!candidate->flushed) { - - int combined = 0; - - /* Build up rect as much as possible */ - for (j=i; j < surface->bitmap_queue_length; j++) { - - if (!candidate->flushed) { - - /* Clip candidate within current bounds */ - __guac_common_bound_rect(surface, &candidate->rect, NULL, NULL); - if (candidate->rect.width <= 0 || candidate->rect.height <= 0) - candidate->flushed = 1; - - /* Combine if reasonable */ - else if (__guac_common_should_combine(surface, &candidate->rect, 0) || !surface->dirty) { - __guac_common_mark_dirty(surface, &candidate->rect); - candidate->flushed = 1; - combined++; - } - - } - - candidate++; - - } - - /* Re-add to queue if there's room and this update was modified or we expect others might be */ - if ((combined > 1 || i < original_queue_length) - && surface->bitmap_queue_length < GUAC_COMMON_SURFACE_QUEUE_SIZE) - __guac_common_surface_flush_to_queue(surface); - - /* Flush as bitmap otherwise */ - else if (surface->dirty) { - - flushed++; - - int opaque = __guac_common_surface_is_opaque(surface, - &surface->dirty_rect); - - /* Prefer WebP when reasonable */ - if (__guac_common_surface_should_use_webp(surface, - &surface->dirty_rect)) - __guac_common_surface_flush_to_webp(surface, opaque); - - /* If not WebP, JPEG is the next best (lossy) choice */ - else if (opaque && __guac_common_surface_should_use_jpeg( - surface, &surface->dirty_rect)) - __guac_common_surface_flush_to_jpeg(surface); - - /* Use PNG if no lossy formats are appropriate */ - else - __guac_common_surface_flush_to_png(surface, opaque); - - } - - } - - current++; - - } - - /* Flush complete */ - surface->bitmap_queue_length = 0; - -} - -void guac_common_surface_flush(guac_common_surface* surface) { - - pthread_mutex_lock(&surface->_lock); - - /* Flush any applicable layer properties */ - __guac_common_surface_flush_properties(surface); - - /* Flush surface contents */ - __guac_common_surface_flush(surface); - - pthread_mutex_unlock(&surface->_lock); - -} - -void guac_common_surface_dup(guac_common_surface* surface, - guac_client* client, guac_socket* socket) { - - pthread_mutex_lock(&surface->_lock); - - /* Do nothing if not realized */ - if (!surface->realized) - goto complete; - - /* Synchronize layer-specific properties if applicable */ - if (surface->layer->index > 0) { - - /* Synchronize opacity */ - guac_protocol_send_shade(socket, surface->layer, surface->opacity); - - /* Synchronize location and hierarchy */ - guac_protocol_send_move(socket, surface->layer, - surface->parent, surface->x, surface->y, surface->z); - - } - - /* Synchronize multi-touch support level */ - else if (surface->layer->index == 0) - guac_protocol_send_set_int(socket, surface->layer, - GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, - surface->touches); - - /* Sync size to new socket */ - guac_protocol_send_size(socket, surface->layer, - surface->width, surface->height); - - /* Send contents of layer, if non-empty */ - if (surface->width > 0 && surface->height > 0) { - - /* Get entire surface */ - cairo_surface_t* rect = cairo_image_surface_create_for_data( - surface->buffer, CAIRO_FORMAT_ARGB32, - surface->width, surface->height, surface->stride); - - /* Send PNG for rect */ - guac_client_stream_png(client, socket, GUAC_COMP_OVER, surface->layer, - 0, 0, rect); - cairo_surface_destroy(rect); - - } - -complete: - pthread_mutex_unlock(&surface->_lock); - -} diff --git a/src/common/tests/Makefile.am b/src/common/tests/Makefile.am index 27ac75cd1..b84dc827f 100644 --- a/src/common/tests/Makefile.am +++ b/src/common/tests/Makefile.am @@ -39,12 +39,6 @@ noinst_HEADERS = \ test_common_SOURCES = \ iconv/convert.c \ iconv/convert-test-data.c \ - rect/clip_and_split.c \ - rect/constrain.c \ - rect/expand_to_grid.c \ - rect/extend.c \ - rect/init.c \ - rect/intersects.c \ string/count_occurrences.c \ string/split.c diff --git a/src/common/tests/rect/clip_and_split.c b/src/common/tests/rect/clip_and_split.c deleted file mode 100644 index e286bce9f..000000000 --- a/src/common/tests/rect/clip_and_split.c +++ /dev/null @@ -1,156 +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/rect.h" - -#include - -/** - * Test which verifies that guac_common_rect_clip_and_split() divides a - * rectangle into subrectangles after removing a "hole" rectangle. - */ -void test_rect__clip_and_split() { - - int res; - - guac_common_rect cut; - guac_common_rect min; - guac_common_rect rect; - - guac_common_rect_init(&min, 10, 10, 10, 10); - - /* Clip top */ - guac_common_rect_init(&rect, 10, 5, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(5, cut.y); - CU_ASSERT_EQUAL(10, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(5, rect.height); - - /* Clip bottom */ - guac_common_rect_init(&rect, 10, 15, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(20, cut.y); - CU_ASSERT_EQUAL(10, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(15, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(5, rect.height); - - /* Clip left */ - guac_common_rect_init(&rect, 5, 10, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(5, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Clip right */ - guac_common_rect_init(&rect, 15, 10, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(20, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(15, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(5, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* - * Test a rectangle which completely covers the hole. - * Clip and split until done. - */ - guac_common_rect_init(&rect, 5, 5, 20, 20); - - /* Clip top */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(5, cut.y); - CU_ASSERT_EQUAL(20, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(5, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(20, rect.width); - CU_ASSERT_EQUAL(15, rect.height); - - /* Clip left */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(15, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(15, rect.width); - CU_ASSERT_EQUAL(15, rect.height); - - /* Clip bottom */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(20, cut.y); - CU_ASSERT_EQUAL(15, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(15, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Clip right */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(20, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Make sure nothing is left to do */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(0, res); - -} - diff --git a/src/common/tests/rect/constrain.c b/src/common/tests/rect/constrain.c deleted file mode 100644 index 793aac224..000000000 --- a/src/common/tests/rect/constrain.c +++ /dev/null @@ -1,43 +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/rect.h" - -#include - -/** - * Test which verifies that guac_common_rect_constrain() restricts a given - * rectangle to arbitrary bounds. - */ -void test_rect__constrain() { - - guac_common_rect max; - guac_common_rect rect; - - guac_common_rect_init(&rect, -10, -10, 110, 110); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_constrain(&rect, &max); - - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(100, rect.width); - CU_ASSERT_EQUAL(100, rect.height); - -} - diff --git a/src/common/tests/rect/expand_to_grid.c b/src/common/tests/rect/expand_to_grid.c deleted file mode 100644 index beef87d89..000000000 --- a/src/common/tests/rect/expand_to_grid.c +++ /dev/null @@ -1,71 +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/rect.h" - -#include - -/** - * Test which verifies guac_common_rect_expand_to_grid() properly shifts and - * resizes rectangles to fit an NxN grid. - */ -void test_rect__expand_to_grid() { - - int cell_size = 16; - - guac_common_rect max; - guac_common_rect rect; - - /* Simple adjustment */ - guac_common_rect_init(&rect, 0, 0, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - /* Adjustment with moving of rect */ - guac_common_rect_init(&rect, 75, 75, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(max.width - 32, rect.x); - CU_ASSERT_EQUAL(max.height - 32, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - guac_common_rect_init(&rect, -5, -5, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - /* Adjustment with moving and clamping of rect */ - guac_common_rect_init(&rect, 0, 0, 25, 15); - guac_common_rect_init(&max, 0, 5, 32, 15); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(max.x, rect.x); - CU_ASSERT_EQUAL(max.y, rect.y); - CU_ASSERT_EQUAL(max.width, rect.width); - CU_ASSERT_EQUAL(max.height, rect.height); - -} - diff --git a/src/common/tests/rect/extend.c b/src/common/tests/rect/extend.c deleted file mode 100644 index dc2a0e308..000000000 --- a/src/common/tests/rect/extend.c +++ /dev/null @@ -1,42 +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/rect.h" - -#include - -/** - * Test which verifies that guac_common_rect_extend() expands the given - * rectangle as necessary to contain at least the given bounds. - */ -void test_rect__extend() { - - guac_common_rect max; - guac_common_rect rect; - - guac_common_rect_init(&rect, 10, 10, 90, 90); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_extend(&rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(100, rect.width); - CU_ASSERT_EQUAL(100, rect.height); - -} - diff --git a/src/common/tests/rect/init.c b/src/common/tests/rect/init.c deleted file mode 100644 index 288cd751b..000000000 --- a/src/common/tests/rect/init.c +++ /dev/null @@ -1,39 +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/rect.h" - -#include - -/** - * Test which verifies rectangle initialization via guac_common_rect_init(). - */ -void test_rect__init() { - - guac_common_rect max; - - guac_common_rect_init(&max, 0, 0, 100, 100); - - CU_ASSERT_EQUAL(0, max.x); - CU_ASSERT_EQUAL(0, max.y); - CU_ASSERT_EQUAL(100, max.width); - CU_ASSERT_EQUAL(100, max.height); - -} - diff --git a/src/common/tests/rect/intersects.c b/src/common/tests/rect/intersects.c deleted file mode 100644 index c48026844..000000000 --- a/src/common/tests/rect/intersects.c +++ /dev/null @@ -1,91 +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/rect.h" - -#include - -/** - * Test which verifies intersection testing via guac_common_rect_intersects(). - */ -void test_rect__intersects() { - - int res; - - guac_common_rect min; - guac_common_rect rect; - - guac_common_rect_init(&min, 10, 10, 10, 10); - - /* Rectangle intersection - empty - * rectangle is outside */ - guac_common_rect_init(&rect, 25, 25, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(0, res); - - /* Rectangle intersection - complete - * rectangle is completely inside */ - guac_common_rect_init(&rect, 11, 11, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects UL */ - guac_common_rect_init(&rect, 8, 8, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - partial - * rectangle intersects LR */ - guac_common_rect_init(&rect, 18, 18, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - complete - * rect intersects along UL but inside */ - guac_common_rect_init(&rect, 10, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects along L but outside */ - guac_common_rect_init(&rect, 5, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - complete - * rectangle intersects along LR but rest is inside */ - guac_common_rect_init(&rect, 15, 15, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects along R but rest is outside */ - guac_common_rect_init(&rect, 20, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - partial - * rectangle encloses min; which is a partial intersection */ - guac_common_rect_init(&rect, 5, 5, 20, 20); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - -} - diff --git a/src/protocols/vnc/user.c b/src/protocols/vnc/user.c index 7c813a94f..7025be1f9 100644 --- a/src/protocols/vnc/user.c +++ b/src/protocols/vnc/user.c @@ -21,8 +21,6 @@ #include "clipboard.h" #include "input.h" -#include "common/dot_cursor.h" -#include "common/pointer_cursor.h" #include "user.h" #include "sftp.h" #include "vnc.h" From 8003836e7a7dea8952bb521d61b87bc7fb748a67 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Sep 2024 01:01:48 -0700 Subject: [PATCH 28/53] GUACAMOLE-377: Ensure frame boundaries are sent even for frames containing no graphics. --- src/libguac/display-flush.c | 88 +++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index b53af93f2..863d6ff18 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -101,10 +101,15 @@ static void* LFR_guac_display_broadcast_cursor_state(guac_user* user, void* data * @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 void PFW_LFW_guac_display_frame_complete(guac_display* display) { +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; @@ -126,6 +131,8 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { current->last_frame.dirty = current->pending_frame.dirty; current->pending_frame.dirty = (guac_rect) { 0 }; + retval = 1; + } /* Commit any change in layer size */ @@ -138,6 +145,8 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { current->last_frame.width = current->pending_frame.width; current->last_frame.height = current->pending_frame.height; + retval = 1; + } /* Commit any change in layer opacity */ @@ -148,6 +157,8 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { current->last_frame.opacity = current->pending_frame.opacity; + retval = 1; + } /* Commit any change in layer location/hierarchy */ @@ -167,6 +178,8 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { 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 */ @@ -218,8 +231,12 @@ static void PFW_LFW_guac_display_frame_complete(guac_display* display) { 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) { @@ -264,36 +281,38 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) { * passes. */ GUAC_DISPLAY_PLAN_BEGIN_PHASE(); plan = PFW_LFR_guac_display_plan_create(display); - if (plan == NULL) - goto finished_with_last_frame_lock; GUAC_DISPLAY_PLAN_END_PHASE(display, "draft", 1, 5); - display->pending_frame.timestamp = plan->frame_end; + if (plan != NULL) { - /* 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); + 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); - /* 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 @@ -302,11 +321,22 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) { * without disturbing the encoding performed by the worker threads. */ + int frame_nonempty; + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); - PFW_LFW_guac_display_frame_complete(display); + frame_nonempty = PFW_LFW_guac_display_frame_complete(display); GUAC_DISPLAY_PLAN_END_PHASE(display, "commit", 5, 5); -finished_with_last_frame_lock: + /* 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: From 3aae6035902b262abab332ecbe2de5c19788ab0f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Sep 2024 15:33:44 -0700 Subject: [PATCH 29/53] GUACAMOLE-377: Migrate from timer to thread for pending users. --- src/libguac/client.c | 165 +++++++++------------------------ src/libguac/guacamole/client.h | 17 ++-- 2 files changed, 51 insertions(+), 131 deletions(-) diff --git a/src/libguac/client.c b/src/libguac/client.c index e0783439b..efe6d53ae 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 @@ -161,28 +161,10 @@ void guac_client_free_stream(guac_client* client, guac_stream* stream) { * 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)); @@ -245,10 +227,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; } @@ -296,12 +297,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); @@ -312,6 +307,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)); @@ -324,6 +322,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)); @@ -355,19 +358,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)); @@ -445,12 +435,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; @@ -467,82 +464,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/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 From 7e593c00beb23c529bce167762bf1bce0d49a308 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 8 Sep 2024 18:10:05 -0700 Subject: [PATCH 30/53] GUACAMOLE-377: Restore heuristic detection of RDP frame boundaries. --- src/protocols/rdp/client.h | 16 ++++++---- src/protocols/rdp/gdi.c | 34 +-------------------- src/protocols/rdp/gdi.h | 18 ++--------- src/protocols/rdp/pointer.c | 36 ++-------------------- src/protocols/rdp/rdp.c | 61 +++++++++++++++---------------------- src/protocols/rdp/rdp.h | 5 --- 6 files changed, 41 insertions(+), 129 deletions(-) diff --git a/src/protocols/rdp/client.h b/src/protocols/rdp/client.h index 943d39abb..9b307878b 100644 --- a/src/protocols/rdp/client.h +++ b/src/protocols/rdp/client.h @@ -23,16 +23,20 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The maximum duration of a frame in milliseconds. This ensures we at least + * meet a reasonable minimum framerate in the case that the RDP server provides + * no frame boundaries and streams data continuously enough that frame + * boundaries are not discernable through timing. */ -#define GUAC_RDP_FRAME_DURATION 60 +#define GUAC_RDP_MAX_FRAME_DURATION 33 /** - * 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. + * The minimum duration of a frame in milliseconds. This ensures we don't start + * flushing a ton of tiny frames if an RDP server provides no frame boundaries + * and streams data inconsistently enough that timing would suggest frame + * boundaries in the middle of a frame. */ -#define GUAC_RDP_FRAME_TIMEOUT 0 +#define GUAC_RDP_MIN_FRAME_DURATION 10 /** * The amount of time to wait for a new message from the RDP server when diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index 3b1a973eb..d96f76902 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -38,30 +38,16 @@ 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->in_frame = 0; 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_display_end_multiple_frames(rdp_client->display, rdp_client->frames_received); - guac_socket_flush(client->socket); - rdp_client->frames_received = 0; - } - } BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* frame_marker) { @@ -88,19 +74,6 @@ BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_ } -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; - - /* 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); - - return TRUE; - -} - BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; @@ -135,11 +108,6 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_display_layer_close_raw(default_layer, dst_context); - /* Next frame */ - if (gdi->inGfxFrame) { - guac_rdp_gdi_mark_frame(context, 0); - } - return TRUE; } diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h index 1f63a8fa2..24593db09 100644 --- a/src/protocols/rdp/gdi.h +++ b/src/protocols/rdp/gdi.h @@ -72,22 +72,8 @@ BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* fr BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker); /** - * Handler called when a paint operation is beginning. This function is - * expected to be called by the FreeRDP GDI implementation of RemoteFX when a - * new frame has started. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -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/pointer.c b/src/protocols/rdp/pointer.c index e22e68d52..1a4f3f807 100644 --- a/src/protocols/rdp/pointer.c +++ b/src/protocols/rdp/pointer.c @@ -75,14 +75,6 @@ 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; - - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 1); - guac_display_layer* src_layer = ((guac_rdp_pointer*) pointer)->layer; guac_display_layer_raw_context* src_context = guac_display_layer_open_raw(src_layer); @@ -111,9 +103,7 @@ BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* poi guac_display_layer_close_raw(cursor_layer, dst_context); guac_display_layer_close_raw(src_layer, src_context); - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); - + guac_display_end_mouse_frame(rdp_client->display); return TRUE; } @@ -135,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_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_NONE); - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); - + guac_display_end_mouse_frame(rdp_client->display); return TRUE; } @@ -158,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_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); - if (rdp_client->frames_supported && !in_frame) - guac_rdp_gdi_mark_frame(context, 0); - + guac_display_end_mouse_frame(rdp_client->display); return TRUE; } diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 98f682e15..56206d0aa 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -214,7 +214,6 @@ static BOOL rdp_freerdp_pre_connect(freerdp* instance) { /* Set up GDI */ 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->SurfaceFrameMarker = guac_rdp_gdi_surface_frame_marker; @@ -581,18 +580,14 @@ 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); + 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 */ + guac_timestamp frame_start = guac_timestamp_current(); + int frames_at_start = rdp_client->frames_received; 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)); @@ -605,34 +600,29 @@ static int guac_rdp_handle_connection(guac_client* client) { 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; + int frame_duration = guac_timestamp_current() - frame_start; + + if (!rdp_client->in_frame) { + + /* Flush frame if at least one frame has been produced */ + if (rdp_client->frames_received > frames_at_start) + break; + + /* Continue processing messages for up to a reasonable + * minimum framerate without an explicit frame boundary + * indicating that the frame is not yet complete */ + if (frame_duration > GUAC_RDP_MAX_FRAME_DURATION) + break; + } - /* 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; + /* Do not exceed a reasonable maximum framerate without an + * explicit frame boundary terminating the frame early */ + int allowed_wait = GUAC_RDP_MIN_FRAME_DURATION - frame_duration; + if (allowed_wait < 0) + allowed_wait = 0; + + wait_result = rdp_guac_client_wait_for_messages(client, allowed_wait); } while (wait_result > 0); @@ -657,9 +647,8 @@ static int guac_rdp_handle_connection(guac_client* client) { /* 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) { + else if (!rdp_client->in_frame) { guac_display_end_multiple_frames(rdp_client->display, rdp_client->frames_received); - guac_socket_flush(client->socket); rdp_client->frames_received = 0; } diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 517e9e471..d6f88bea1 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -106,11 +106,6 @@ typedef struct guac_rdp_client { */ guac_display_layer* current_surface; - /** - * Whether the RDP server supports defining explicit frame boundaries. - */ - int frames_supported; - /** * Whether the RDP server has reported that a new frame is in progress, and * we are now receiving updates relevant to that frame. From dc833c74a1d2b98592434807865b29faa97145c2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 8 Sep 2024 18:12:15 -0700 Subject: [PATCH 31/53] GUACAMOLE-377: Restore heuristic detection of VNC frame boundaries. --- src/protocols/vnc/client.h | 16 ++++++----- src/protocols/vnc/display.c | 9 ------- src/protocols/vnc/display.h | 11 -------- src/protocols/vnc/vnc.c | 54 ++++++++++++++++++++++++++----------- 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/protocols/vnc/client.h b/src/protocols/vnc/client.h index 1120a4386..dd0ec7413 100644 --- a/src/protocols/vnc/client.h +++ b/src/protocols/vnc/client.h @@ -23,16 +23,20 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The maximum duration of a frame in milliseconds. This ensures we at least + * meet a reasonable minimum framerate in the case that the VNC server provides + * no frame boundaries and streams data continuously enough that frame + * boundaries are not discernable through timing. */ -#define GUAC_VNC_FRAME_DURATION 40 +#define GUAC_VNC_MAX_FRAME_DURATION 33 /** - * 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. + * The minimum duration of a frame in milliseconds. This ensures we don't start + * flushing a ton of tiny frames if an VNC server provides no frame boundaries + * and streams data inconsistently enough that timing would suggest frame + * boundaries in the middle of a frame. */ -#define GUAC_VNC_FRAME_TIMEOUT 0 +#define GUAC_VNC_MIN_FRAME_DURATION 10 /** * The amount of time to wait for a new message from the VNC server when diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 076e00ca0..1589c5ee0 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -133,15 +133,6 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { } -void guac_vnc_update_finished(rfbClient* client) { - - guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); - guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - - guac_display_end_multiple_frames(vnc_client->display, 1); - -} - void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); diff --git a/src/protocols/vnc/display.h b/src/protocols/vnc/display.h index fd1bcd04d..cd7d27b25 100644 --- a/src/protocols/vnc/display.h +++ b/src/protocols/vnc/display.h @@ -51,17 +51,6 @@ */ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h); -/** - * Callback invoked by libVNCServer when all binary image data for the current - * frame has been received from the VNC server. The image data that frame will - * have been exposed via previous calls to guac_vnc_update(). - * - * @param client - * The VNC client associated with the VNC session in which the new image - * was received. - */ -void guac_vnc_update_finished(rfbClient* client); - /** * Callback invoked by libVNCServer when it receives a CopyRect message. * CopyRect specified a rectangle of source data within the display and a diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index 85f0cbf3c..e3b1776e3 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -139,7 +139,6 @@ rfbClient* guac_vnc_get_client(guac_client* client) { /* Framebuffer update handler */ rfb_client->GotFrameBufferUpdate = guac_vnc_update; - rfb_client->FinishedFrameBufferUpdate = guac_vnc_update_finished; rfb_client->GotCopyRect = guac_vnc_copyrect; #ifdef ENABLE_VNC_TLS_LOCKING @@ -252,20 +251,20 @@ 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); } @@ -548,21 +547,46 @@ void* guac_vnc_client_thread(void* data) { /* Handle messages from VNC server while client is running */ while (client->state == GUAC_CLIENT_RUNNING) { - /* Wait for start of frame */ + /* Wait for data and construct a reasonable frame */ int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_FRAME_START_TIMEOUT); - if (wait_result == 0) - continue; - - /* Handle any message received */ - if (!HandleRFBServerMessage(rfb_client)) { - guac_client_abort(client, - GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, - "Error handling message from VNC server."); - break; + if (wait_result > 0) { + + /* Read server messages until frame is built */ + guac_timestamp frame_start = guac_timestamp_current(); + do { + + /* Handle any message received */ + if (!HandleRFBServerMessage(rfb_client)) { + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, + "Error handling message from VNC server."); + break; + } + + int frame_duration = guac_timestamp_current() - frame_start; + + /* Continue processing messages for up to a reasonable minimum + * framerate without an explicit frame boundary indicating that + * the frame is not yet complete */ + if (frame_duration > GUAC_VNC_MAX_FRAME_DURATION) + break; + + /* Do not exceed a reasonable maximum framerate without an + * explicit frame boundary terminating the frame early */ + int allowed_wait = GUAC_VNC_MIN_FRAME_DURATION - frame_duration; + if (allowed_wait < 0) + allowed_wait = 0; + + wait_result = guac_vnc_wait_for_messages(rfb_client, allowed_wait); + + } while (wait_result > 0); + + guac_display_end_frame(vnc_client->display); + } /* 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."); } From cbfeae6cf5e613d3d346663c68f65b042cd2a90a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 11 Sep 2024 11:44:32 -0700 Subject: [PATCH 32/53] GUACAMOLE-377: Allow the pending frame buffer for a guac_display_layer to be external. --- src/libguac/display-flush.c | 35 +++++++++- src/libguac/display-layer-list.c | 106 +++++++++++-------------------- src/libguac/display-layer.c | 40 ++++++++++++ src/libguac/display-priv.h | 19 ++++++ src/libguac/guacamole/display.h | 36 ++++++++++- 5 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index 863d6ff18..289460d0a 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -115,8 +115,39 @@ static int PFW_LFW_guac_display_frame_complete(guac_display* display) { guac_display_layer* current = display->pending_frame.layers; while (current != NULL) { - /* Copy over pending frame contents if actually changed */ - if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + /* 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; diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c index c6f8ac5dc..350f7720c 100644 --- a/src/libguac/display-layer-list.c +++ b/src/libguac/display-layer-list.c @@ -118,91 +118,50 @@ static void guac_imgcpy(void* dst, size_t dst_stride, int dst_width, int dst_hei * @param height * The new height, in pixels. */ -static void PFW_LFW_guac_display_layer_buffers_resize(guac_display_layer_state* last_frame, - guac_display_layer_state* pending_frame, int width, int height) { +static void XFW_guac_display_layer_buffer_resize(guac_display_layer_state* frame_state, + int width, int height) { - GUAC_ASSERT(last_frame->buffer_width == pending_frame->buffer_width); - GUAC_ASSERT(last_frame->buffer_height == pending_frame->buffer_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 == last_frame->buffer_width - && height == last_frame->buffer_height) + if (width == frame_state->buffer_width + && height == frame_state->buffer_height) return; - /* The request to resize applies only to the pending frame, but space for - * the last frame must be maintained. If either requested dimension is - * smaller than the last frame dimensions, the relevant dimension of the - * last frame must be used instead. */ - - int new_buffer_width = last_frame->buffer_width; - if (width > new_buffer_width) - new_buffer_width = width; - - int new_buffer_height = last_frame->buffer_height; - if (height > new_buffer_height) - new_buffer_height = height; - - /* Determine details of shared buffer space sufficient for both the - * established last frame and the resized pending frame. Allocate new - * shared buffer space for last and pending frames, interleaving their - * rows. - * - * NOTE: We interleave the rows of the last and pending frames to promote - * locality of reference. The comparisons performed between last and - * pending frames to determine what has changed are faster when the rows - * are interleaved, as data relevant to those comparisons will tend to be - * present in the CPU cache. */ - - int new_last_frame_offset = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, new_buffer_width); - int new_common_stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, new_last_frame_offset * 2); - unsigned char* new_buffer_base = guac_mem_zalloc(new_buffer_height, new_common_stride); - unsigned char* new_pending_frame_buffer = new_buffer_base; - unsigned char* new_last_frame_buffer = new_buffer_base + new_last_frame_offset; + 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 (last_frame->buffer != NULL && pending_frame->buffer != NULL) { + if (frame_state->buffer != NULL) { guac_imgcpy( - /* Copy to newly-allocated pending frame buffer ... */ - new_pending_frame_buffer, new_common_stride, - new_buffer_width, new_buffer_height, + /* Copy to newly-allocated frame buffer ... */ + buffer, stride, + width, height, - /* ... from old pending frame buffer. */ - pending_frame->buffer, pending_frame->buffer_stride, - pending_frame->buffer_width, pending_frame->buffer_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_imgcpy( - - /* Copy to newly-allocated last frame buffer ... */ - new_last_frame_buffer, new_common_stride, - last_frame->buffer_width, last_frame->buffer_height, - - /* ... from old last frame buffer. */ - last_frame->buffer, last_frame->buffer_stride, - last_frame->buffer_width, last_frame->buffer_height, - - /* All pixels are 32-bit */ - GUAC_DISPLAY_LAYER_RAW_BPP); + guac_mem_free(frame_state->buffer); } - guac_mem_free(pending_frame->buffer); - last_frame->buffer = new_buffer_base + new_last_frame_offset; - pending_frame->buffer = new_buffer_base; - - last_frame->buffer_width = pending_frame->buffer_width = new_buffer_width; - last_frame->buffer_height = pending_frame->buffer_height = new_buffer_height; - last_frame->buffer_stride = pending_frame->buffer_stride = new_common_stride; + frame_state->buffer = buffer; + frame_state->buffer_width = width; + frame_state->buffer_height = height; + frame_state->buffer_stride = stride; } @@ -227,10 +186,10 @@ static void PFW_LFW_guac_display_layer_state_init(guac_display_layer_state* last last_frame->opacity = pending_frame->opacity = 0xFF; last_frame->parent = pending_frame->parent = GUAC_DEFAULT_LAYER; - /* Allocate shared buffer space for last and pending frames, interleaving - * their rows */ + XFW_guac_display_layer_buffer_resize(last_frame, + last_frame->width, last_frame->height); - PFW_LFW_guac_display_layer_buffers_resize(last_frame, pending_frame, + XFW_guac_display_layer_buffer_resize(pending_frame, pending_frame->width, pending_frame->height); } @@ -395,11 +354,14 @@ void guac_display_remove_layer(guac_display_layer* display_layer) { } - /* Free memory for underlying image surface and change tracking cells - * (NOTE: Freeing pending_frame.buffer inherently also frees - * last_frame.buffer because they are actually interleaved views of the - * same block) */ - guac_mem_free(display_layer->pending_frame.buffer); + /* 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); @@ -421,7 +383,11 @@ void PFW_LFW_guac_display_layer_resize(guac_display_layer* layer, int width, int } - PFW_LFW_guac_display_layer_buffers_resize(&layer->last_frame, &layer->pending_frame, width, height); + /* 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; diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index e95fc5cac..83b96aea0 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -218,6 +218,46 @@ void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_ 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); diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 823dfc8e6..42d1f326b 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -63,6 +63,18 @@ * 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. @@ -347,6 +359,13 @@ typedef struct guac_display_layer_state { */ 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 diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index ea4e9f32f..b117fd6ce 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -124,6 +124,12 @@ struct guac_display_layer_raw_context { * 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 manualy-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. */ unsigned char* buffer; @@ -132,12 +138,28 @@ struct guac_display_layer_raw_context { * 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; @@ -164,10 +186,16 @@ struct guac_display_layer_raw_context { /** * 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_defaulta_layer() and + * 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. @@ -520,6 +548,12 @@ void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches); * 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 From fa37f006284681be58306f9372ed1cdf61368672 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Sep 2024 22:35:17 -0700 Subject: [PATCH 33/53] GUACAMOLE-377: Allow external guac_display buffers to be replaced with NULL if necessary for external cleanup tasks. --- src/libguac/display-flush.c | 10 ++++++++++ src/libguac/display-layer.c | 8 ++++++++ src/libguac/display-plan-rect.c | 7 ++++++- src/libguac/display-plan.c | 9 +++++++++ src/libguac/guacamole/display.h | 8 +++++++- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index 289460d0a..9fd76dfa6 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -19,6 +19,7 @@ #include "display-plan.h" #include "display-priv.h" +#include "guacamole/assert.h" #include "guacamole/client.h" #include "guacamole/display.h" #include "guacamole/fifo.h" @@ -115,6 +116,15 @@ static int PFW_LFW_guac_display_frame_complete(guac_display* display) { 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 diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index 83b96aea0..caa2113b7 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -18,6 +18,7 @@ */ #include "display-priv.h" +#include "guacamole/assert.h" #include "guacamole/display.h" #include "guacamole/rect.h" #include "guacamole/rwlock.h" @@ -274,6 +275,13 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay 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 }; diff --git a/src/libguac/display-plan-rect.c b/src/libguac/display-plan-rect.c index bad1253a4..c2a4196a2 100644 --- a/src/libguac/display-plan-rect.c +++ b/src/libguac/display-plan-rect.c @@ -215,7 +215,12 @@ void PFR_guac_display_plan_rewrite_as_rects(guac_display_plan* plan) { size_t stride = layer->pending_frame.buffer_stride; const unsigned char* buffer = layer->pending_frame.buffer; - if (guac_display_plan_is_rect_single_color(buffer, stride, &op->dest, &color)) { + /* 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) diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c index e8091250d..ec1bbead9 100644 --- a/src/libguac/display-plan.c +++ b/src/libguac/display-plan.c @@ -147,6 +147,15 @@ guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { 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 diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index b117fd6ce..333b504dd 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -126,10 +126,16 @@ struct guac_display_layer_raw_context { * alpha. * * This value may be replaced with a manually-allocated buffer if the - * associated layer should instead use that manualy-allocated buffer for + * 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; From d31a96c62b77c218258c60370b3ef5113b6e7680 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 11 Sep 2024 11:54:41 -0700 Subject: [PATCH 34/53] GUACAMOLE-377: Migrate RDP to direct access of FreeRDP's GDI buffer. --- src/protocols/rdp/gdi.c | 82 +++++++++++++++++++++++++++++++++-------- src/protocols/rdp/gdi.h | 13 +++++++ src/protocols/rdp/rdp.c | 54 +++++++++++++++++++++------ src/protocols/rdp/rdp.h | 8 ++++ 4 files changed, 130 insertions(+), 27 deletions(-) diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index d96f76902..8a9869595 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -18,7 +18,6 @@ */ #include "color.h" -#include "guacamole/display.h" #include "rdp.h" #include "settings.h" @@ -27,7 +26,9 @@ #include #include #include +#include #include +#include #include #include @@ -74,39 +75,69 @@ BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_ } +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); + + /* 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; + +} + BOOL guac_rdp_gdi_end_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_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; - guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); - guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(default_layer); + /* 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, &dst_context->bounds); - - guac_display_layer_raw_context_put(dst_context, &dst_rect, - GUAC_RECT_CONST_BUFFER(dst_rect, gdi->primary_buffer, gdi->stride, 4), - gdi->stride); + guac_rect_constrain(&dst_rect, ¤t_context->bounds); + guac_rect_extend(¤t_context->dirty, &dst_rect); - guac_rect_extend(&dst_context->dirty, &dst_rect); +paint_complete: - guac_display_layer_close_raw(default_layer, dst_context); + /* There will be no further drawing operations */ + rdp_client->current_context = NULL; + guac_display_layer_close_raw(default_layer, current_context); return TRUE; @@ -116,13 +147,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_display_layer_resize(guac_display_default_layer(rdp_client->display), width, height); - guac_client_log(client, GUAC_LOG_DEBUG, "Server resized display to %ix%i", width, height); + GUAC_ASSERT(rdp_client->current_context == NULL); + + /* 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", + gdi->width, gdi->height); + + guac_display_layer_close_raw(default_layer, current_context); - return gdi_resize(context->gdi, width, height); + return retval; } diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h index 24593db09..c6cfeb62a 100644 --- a/src/protocols/rdp/gdi.h +++ b/src/protocols/rdp/gdi.h @@ -71,6 +71,19 @@ BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* fr */ BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker); +/** + * Handler called when a paint operation is beginning. This function is + * expected to be called by the FreeRDP GDI implementation of RemoteFX when a + * new frame has started. + * + * @param context + * The rdpContext associated with the current RDP session. + * + * @return + * TRUE if successful, FALSE otherwise. + */ +BOOL guac_rdp_gdi_begin_paint(rdpContext* context); + /** * Handler called when FreeRDP has finished performing updates to the backing * surface of its GDI (graphics) implementation. diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 56206d0aa..74f2d266b 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -214,6 +214,7 @@ static BOOL rdp_freerdp_pre_connect(freerdp* instance) { /* Set up GDI */ 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->SurfaceFrameMarker = guac_rdp_gdi_surface_frame_marker; @@ -464,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 @@ -589,13 +614,9 @@ static int guac_rdp_handle_connection(guac_client* client) { do { /* 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) { + * messages being sent), aborting if FreeRDP event handling + * fails */ + if (!guac_rdp_handle_events(rdp_client)) { wait_result = -1; break; } @@ -667,9 +688,22 @@ 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 */ + /* 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); @@ -685,10 +719,6 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_keyboard_free(rdp_client->keyboard); rdp_client->keyboard = NULL; - /* Free display */ - guac_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 d6f88bea1..5d723c30a 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -106,6 +106,14 @@ typedef struct guac_rdp_client { */ guac_display_layer* current_surface; + /** + * 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. + */ + 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. From b667f697e4aca849435f2a32313fe8dadc365341 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 11 Sep 2024 14:02:00 -0700 Subject: [PATCH 35/53] GUACAMOLE-377: Migrate VNC to direct access of RFB framebuffer. --- src/protocols/vnc/display.c | 145 +++++++++++++++++++++++------------- src/protocols/vnc/vnc.c | 1 + src/protocols/vnc/vnc.h | 6 ++ 3 files changed, 101 insertions(+), 51 deletions(-) diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 1589c5ee0..6abbf36d0 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -54,70 +54,94 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { int rfb_height = client->height; int rfb_width = client->width; - /* 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_width, rfb_height); - - /* Begin drawing operation directly to default layer */ - guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + guac_display_layer_raw_context* context; + unsigned int vnc_bpp = client->format.bitsPerPixel / 8; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, rfb_width); /* Convert operation coordinates to guac_rect for easier manipulation */ guac_rect op_bounds; guac_rect_init(&op_bounds, x, y, w, h); - /* Ensure draw is within current bounds of the pending frame */ - guac_rect_constrain(&op_bounds, &context->bounds); + /* Point directly at framebuffer if the pixel format used is identical to + * that expected by guac_display. Resize of the layer is implicit in this + * case. */ + if (vnc_bpp == GUAC_DISPLAY_LAYER_RAW_BPP && !vnc_client->settings->swap_red_blue) { - /* VNC framebuffer */ - unsigned int vnc_bpp = client->format.bitsPerPixel / 8; - size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, client->width); - const unsigned char* vnc_current_row = GUAC_RECT_CONST_BUFFER(op_bounds, client->frameBuffer, vnc_stride, vnc_bpp); + context = guac_display_layer_open_raw(default_layer); + context->buffer = client->frameBuffer; + context->stride = vnc_stride; - 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++) { + /* Update bounds of pending frame to match those of RFB framebuffer */ + guac_rect_init(&context->bounds, 0, 0, rfb_width, rfb_height); - /* Get current Guacamole buffer row, advance to next */ - uint32_t* layer_current_pixel = (uint32_t*) layer_current_row; - layer_current_row += context->stride; + /* Ensure operation bounds are within possibly updated bounds of the + * pending frame (now the RFB client framebuffer) */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Get current VNC framebuffer row, advance to next */ - const unsigned char* vnc_current_pixel = vnc_current_row; - vnc_current_row += vnc_stride; + } - for (int dx = op_bounds.left; dx < op_bounds.right; dx++) { + /* All other framebuffer formats must be manually converted */ + else { - /* Read current VNC pixel value */ - uint32_t v; - switch (vnc_bpp) { - case 4: - v = *((uint32_t*) vnc_current_pixel); - break; + /* 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_width, rfb_height); - case 2: - v = *((uint16_t*) vnc_current_pixel); - break; + /* Begin drawing operation directly to default layer. NOTE: This is + * intentionally after the call to guac_display_layer_resize() to + * ensure the bounds of the resulting context are representative of the + * resize operation. */ + context = guac_display_layer_open_raw(default_layer); - default: - v = *((uint8_t*) vnc_current_pixel); - } + /* Ensure draw is within current bounds of the pending frame */ + guac_rect_constrain(&op_bounds, &context->bounds); + + const unsigned char* vnc_current_row = GUAC_RECT_CONST_BUFFER(op_bounds, client->frameBuffer, 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++) { + + /* 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 VNC framebuffer row, advance to next */ + const unsigned char* vnc_current_pixel = vnc_current_row; + vnc_current_row += vnc_stride; + + for (int dx = op_bounds.left; dx < op_bounds.right; dx++) { + + /* Read current VNC pixel value */ + uint32_t v; + switch (vnc_bpp) { + + case 2: + v = *((uint16_t*) vnc_current_pixel); + break; + + default: + v = *((uint8_t*) vnc_current_pixel); + + } - /* 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); + /* 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) - *(layer_current_pixel++) = 0xFF000000 | (blue << 16) | (green << 8) | red; - else - *(layer_current_pixel++) = 0xFF000000 | (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; - /* Advance to next pixel in VNC framebuffer */ - vnc_current_pixel += vnc_bpp; + /* Advance to next pixel in VNC framebuffer */ + vnc_current_pixel += vnc_bpp; + } } - } + + } /* end manual convert */ /* Mark modified region as dirty */ guac_rect_extend(&context->dirty, &op_bounds); @@ -140,6 +164,10 @@ void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, in 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 @@ -365,11 +393,26 @@ 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_display_layer_resize(guac_display_default_layer(vnc_client->display), - rfb_client->width, rfb_client->height); + /* Resize of underlying buffer must be performed while holding an open raw + * context if the guac_display is active (replacing the underlying + * framebuffer while guac_display may still attempt to flush a pending + * frame is bad news, as that flush may still reference the freed buffer) */ + if (vnc_client->display != NULL) { + + guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); + guac_display_layer_resize(default_layer, rfb_client->width, rfb_client->height); - /* Use original, wrapped proc */ + /* Use original, wrapped proc to resize the buffer maintained by libvncclient */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + rfbBool result = vnc_client->rfb_MallocFrameBuffer(rfb_client); + guac_display_layer_close_raw(default_layer, context); + + return result; + + } + + /* No need to bracket the buffer allocation in a raw context if there's no + * guac_display yet */ return vnc_client->rfb_MallocFrameBuffer(rfb_client); + } diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index e3b1776e3..d47e1510d 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -139,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 diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index 89173a49e..fda6bb427 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -85,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. From 6a7eb6db3e6d235818484a7aa7530ad4f57f1542 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Sep 2024 22:39:53 -0700 Subject: [PATCH 36/53] GUACAMOLE-377: Wrap all VNC message handling within drawing context. It is otherwise difficult to guarantee that all operations touching the pending frame buffer will occur while holding an open raw context, resulting in unstable behavior. --- src/protocols/vnc/display.c | 70 +++++++------------------------------ src/protocols/vnc/vnc.c | 53 +++++++++++++++++++++++++++- src/protocols/vnc/vnc.h | 6 ++++ 3 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 6abbf36d0..70b03be8a 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -51,48 +51,25 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); - int rfb_height = client->height; - int rfb_width = client->width; - - guac_display_layer_raw_context* context; + 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, rfb_width); + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, client->width); /* Convert operation coordinates to guac_rect for easier manipulation */ guac_rect op_bounds; guac_rect_init(&op_bounds, x, y, w, h); - /* Point directly at framebuffer if the pixel format used is identical to - * that expected by guac_display. Resize of the layer is implicit in this - * case. */ - if (vnc_bpp == GUAC_DISPLAY_LAYER_RAW_BPP && !vnc_client->settings->swap_red_blue) { - - context = guac_display_layer_open_raw(default_layer); - context->buffer = client->frameBuffer; - context->stride = vnc_stride; - - /* Update bounds of pending frame to match those of RFB framebuffer */ - guac_rect_init(&context->bounds, 0, 0, rfb_width, rfb_height); - - /* Ensure operation bounds are within possibly updated bounds of the - * pending frame (now the RFB client framebuffer) */ - guac_rect_constrain(&op_bounds, &context->bounds); - - } - - /* All other framebuffer formats must be manually converted */ - else { + /* Ensure operation bounds are within possibly updated bounds of the + * pending frame (now the RFB client framebuffer) */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* 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_width, rfb_height); + /* 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. */ - /* Begin drawing operation directly to default layer. NOTE: This is - * intentionally after the call to guac_display_layer_resize() to - * ensure the bounds of the resulting context are representative of the - * resize operation. */ - context = guac_display_layer_open_raw(default_layer); + /* 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) { /* Ensure draw is within current bounds of the pending frame */ guac_rect_constrain(&op_bounds, &context->bounds); @@ -152,9 +129,6 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { vnc_client->copy_rect_used = 0; } - /* Draw operation is now complete */ - guac_display_layer_close_raw(default_layer, context); - } void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { @@ -393,26 +367,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 of underlying buffer must be performed while holding an open raw - * context if the guac_display is active (replacing the underlying - * framebuffer while guac_display may still attempt to flush a pending - * frame is bad news, as that flush may still reference the freed buffer) */ - if (vnc_client->display != NULL) { - - guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); - guac_display_layer_resize(default_layer, rfb_client->width, rfb_client->height); - - /* Use original, wrapped proc to resize the buffer maintained by libvncclient */ - guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); - rfbBool result = vnc_client->rfb_MallocFrameBuffer(rfb_client); - guac_display_layer_close_raw(default_layer, context); - - return result; - - } - - /* No need to bracket the buffer allocation in a raw context if there's no - * guac_display yet */ + /* Use original, wrapped proc to resize the buffer maintained by + * libvncclient */ return vnc_client->rfb_MallocFrameBuffer(rfb_client); } diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index d47e1510d..cdd21bd63 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -269,6 +269,57 @@ static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int msec_timeout) { } +/** + * 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; + +} + void* guac_vnc_client_thread(void* data) { guac_client* client = (guac_client*) data; @@ -557,7 +608,7 @@ void* guac_vnc_client_thread(void* data) { do { /* Handle any message received */ - if (!HandleRFBServerMessage(rfb_client)) { + if (!guac_vnc_handle_messages(vnc_client)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Error handling message from VNC server."); diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index fda6bb427..7e5676af8 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -107,6 +107,12 @@ typedef struct guac_vnc_client { */ 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; + /** * Internal clipboard. */ From 8c341bcf0a501564aefb4856462f0f2f7a8ca1d7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 17 Sep 2024 20:17:56 -0700 Subject: [PATCH 37/53] GUACAMOLE-377: Remove unnecessary locking of last frame during resize. No longer necessary now that the last and pending frame buffers are not interleaved. --- src/libguac/display-layer-list.c | 2 +- src/libguac/display-layer.c | 4 +--- src/libguac/display-priv.h | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c index 350f7720c..f80aa3a4e 100644 --- a/src/libguac/display-layer-list.c +++ b/src/libguac/display-layer-list.c @@ -368,7 +368,7 @@ void guac_display_remove_layer(guac_display_layer* display_layer) { } -void PFW_LFW_guac_display_layer_resize(guac_display_layer* layer, int width, int height) { +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); diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c index caa2113b7..2c983a157 100644 --- a/src/libguac/display-layer.c +++ b/src/libguac/display-layer.c @@ -138,12 +138,10 @@ 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); - guac_rwlock_acquire_write_lock(&display->last_frame.lock); - PFW_LFW_guac_display_layer_resize(layer, width, height); + PFW_guac_display_layer_resize(layer, width, height); PFW_guac_display_layer_touch(layer); - guac_rwlock_release_lock(&display->last_frame.lock); guac_rwlock_release_lock(&display->pending_frame.lock); } diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 42d1f326b..ca9298f31 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -754,7 +754,7 @@ void guac_display_remove_layer(guac_display_layer* display_layer); * @param height * The new height, in pixels. */ -void PFW_LFW_guac_display_layer_resize(guac_display_layer* layer, +void PFW_guac_display_layer_resize(guac_display_layer* layer, int width, int height); /** From ae52278b33b9bf721ec895139257b86b206ea0e4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Sep 2024 14:53:13 -0700 Subject: [PATCH 38/53] GUACAMOLE-377: Restore good ol' guac_common_surface and guac_common_cursor for use by terminal. --- src/common/Makefile.am | 18 +- src/common/blank_cursor.c | 75 + src/common/common/blank_cursor.h | 64 + src/common/common/cursor.h | 307 ++++ src/common/common/dot_cursor.h | 60 + src/common/common/ibar_cursor.h | 62 + src/common/common/pointer_cursor.h | 60 + src/common/common/rect.h | 143 ++ src/common/common/surface.h | 539 +++++++ src/common/cursor.c | 327 ++++ src/common/dot_cursor.c | 85 + src/common/ibar_cursor.c | 98 ++ src/common/pointer_cursor.c | 96 ++ src/common/rect.c | 266 +++ src/common/surface.c | 2041 ++++++++++++++++++++++++ src/common/tests/Makefile.am | 6 + src/common/tests/rect/clip_and_split.c | 156 ++ src/common/tests/rect/constrain.c | 43 + src/common/tests/rect/expand_to_grid.c | 71 + src/common/tests/rect/extend.c | 42 + src/common/tests/rect/init.c | 39 + src/common/tests/rect/intersects.c | 91 ++ 22 files changed, 4687 insertions(+), 2 deletions(-) create mode 100644 src/common/blank_cursor.c create mode 100644 src/common/common/blank_cursor.h create mode 100644 src/common/common/cursor.h create mode 100644 src/common/common/dot_cursor.h create mode 100644 src/common/common/ibar_cursor.h create mode 100644 src/common/common/pointer_cursor.h create mode 100644 src/common/common/rect.h create mode 100644 src/common/common/surface.h create mode 100644 src/common/cursor.c create mode 100644 src/common/dot_cursor.c create mode 100644 src/common/ibar_cursor.c create mode 100644 src/common/pointer_cursor.c create mode 100644 src/common/rect.c create mode 100644 src/common/surface.c create mode 100644 src/common/tests/rect/clip_and_split.c create mode 100644 src/common/tests/rect/constrain.c create mode 100644 src/common/tests/rect/expand_to_grid.c create mode 100644 src/common/tests/rect/extend.c create mode 100644 src/common/tests/rect/init.c create mode 100644 src/common/tests/rect/intersects.c diff --git a/src/common/Makefile.am b/src/common/Makefile.am index c2be3f59d..6f17b6538 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -31,20 +31,34 @@ SUBDIRS = . tests noinst_HEADERS = \ common/io.h \ + common/blank_cursor.h \ common/clipboard.h \ + common/cursor.h \ common/defaults.h \ + common/dot_cursor.h \ + common/ibar_cursor.h \ common/iconv.h \ common/json.h \ common/list.h \ - common/string.h + common/pointer_cursor.h \ + common/rect.h \ + common/string.h \ + common/surface.h libguac_common_la_SOURCES = \ io.c \ + blank_cursor.c \ clipboard.c \ + cursor.c \ + dot_cursor.c \ + ibar_cursor.c \ iconv.c \ json.c \ list.c \ - string.c + pointer_cursor.c \ + rect.c \ + string.c \ + surface.c libguac_common_la_CFLAGS = \ -Werror -Wall -pedantic \ diff --git a/src/common/blank_cursor.c b/src/common/blank_cursor.c new file mode 100644 index 000000000..c65db0cbf --- /dev/null +++ b/src/common/blank_cursor.c @@ -0,0 +1,75 @@ +/* + * 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 +#include +#include +#include +#include +#include + +/* Dimensions */ +const int guac_common_blank_cursor_width = 1; +const int guac_common_blank_cursor_height = 1; + +/* Format */ +const cairo_format_t guac_common_blank_cursor_format = CAIRO_FORMAT_ARGB32; +const int guac_common_blank_cursor_stride = 4; + +/* Embedded blank cursor graphic */ +unsigned char guac_common_blank_cursor[] = { + + 0x00,0x00,0x00,0x00 + +}; + +void guac_common_set_blank_cursor(guac_user* user) { + + guac_client* client = user->client; + guac_socket* socket = user->socket; + + /* Draw to buffer */ + guac_layer* cursor = guac_client_alloc_buffer(client); + + cairo_surface_t* graphic = cairo_image_surface_create_for_data( + guac_common_blank_cursor, + guac_common_blank_cursor_format, + guac_common_blank_cursor_width, + guac_common_blank_cursor_height, + guac_common_blank_cursor_stride); + + guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, + 0, 0, graphic); + cairo_surface_destroy(graphic); + + /* Set cursor */ + guac_protocol_send_cursor(socket, 0, 0, cursor, 0, 0, + guac_common_blank_cursor_width, + guac_common_blank_cursor_height); + + /* Free buffer */ + guac_client_free_buffer(client, cursor); + + guac_client_log(client, GUAC_LOG_DEBUG, + "Client cursor image set to generic transparent (blank) cursor."); + +} + diff --git a/src/common/common/blank_cursor.h b/src/common/common/blank_cursor.h new file mode 100644 index 000000000..86a4a9ff2 --- /dev/null +++ b/src/common/common/blank_cursor.h @@ -0,0 +1,64 @@ +/* + * 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_BLANK_CURSOR_H +#define GUAC_COMMON_BLANK_CURSOR_H + +#include "config.h" + +#include +#include + +/** + * Width of the embedded transparent (blank) mouse cursor graphic. + */ +extern const int guac_common_blank_cursor_width; + +/** + * Height of the embedded transparent (blank) mouse cursor graphic. + */ +extern const int guac_common_blank_cursor_height; + +/** + * Number of bytes in each row of the embedded transparent (blank) mouse cursor + * graphic. + */ +extern const int guac_common_blank_cursor_stride; + +/** + * The Cairo grapic format of the transparent (blank) mouse cursor graphic. + */ +extern const cairo_format_t guac_common_blank_cursor_format; + +/** + * Embedded transparent (blank) mouse cursor graphic. + */ +extern unsigned char guac_common_blank_cursor[]; + +/** + * Sets the cursor of the remote display to the embedded transparent (blank) + * cursor graphic. + * + * @param user + * The guac_user to send the cursor to. + */ +void guac_common_set_blank_cursor(guac_user* user); + +#endif + diff --git a/src/common/common/cursor.h b/src/common/common/cursor.h new file mode 100644 index 000000000..96c1c6f23 --- /dev/null +++ b/src/common/common/cursor.h @@ -0,0 +1,307 @@ +/* + * 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_CURSOR_H +#define GUAC_COMMON_CURSOR_H + +#include "surface.h" + +#include +#include +#include +#include + +/** + * The default size of the cursor image buffer. + */ +#define GUAC_COMMON_CURSOR_DEFAULT_SIZE 64*64*4 + +/** + * Cursor object which maintains and synchronizes the current mouse cursor + * state across all users of a specific client. + */ +typedef struct guac_common_cursor { + + /** + * The client to maintain the mouse cursor for. + */ + guac_client* client; + + /** + * The buffer containing the current cursor image. + */ + guac_layer* buffer; + + /** + * The width of the cursor image, in pixels. + */ + int width; + + /** + * The height of the cursor image, in pixels. + */ + int height; + + /** + * Arbitrary image data buffer, backing the Cairo surface used to store + * the cursor image. + */ + unsigned char* image_buffer; + + /** + * The size of the image data buffer, in bytes. + */ + size_t image_buffer_size; + + /** + * The current cursor image, if any. If the mouse cursor has not yet been + * set, this will be NULL. + */ + cairo_surface_t* surface; + + /** + * The X coordinate of the hotspot of the mouse cursor. + */ + int hotspot_x; + + /** + * The Y coordinate of the hotspot of the mouse cursor. + */ + int hotspot_y; + + /** + * The last user to move the mouse, or NULL if no user has moved the + * mouse yet. + */ + guac_user* user; + + /** + * The X coordinate of the current mouse cursor location. + */ + int x; + + /** + * The Y coordinate of the current mouse cursor location. + */ + int y; + + /** + * 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 + */ + int button_mask; + + /** + * The server timestamp representing the point in time when the mouse + * location was last updated. + */ + guac_timestamp timestamp; + + /** + * Lock which restricts simultaneous access to the cursor, guaranteeing + * ordered modifications to the cursor and that incompatible operations + * do not occur simultaneously. This lock is for internal use within the + * cursor only. + */ + pthread_mutex_t _lock; + +} guac_common_cursor; + +/** + * Allocates a new cursor object which maintains and synchronizes the current + * mouse cursor state across all users of the given client. + * + * @param client + * The client for which this object shall maintain the mouse cursor. + * + * @return + * The newly-allocated mouse cursor. + */ +guac_common_cursor* guac_common_cursor_alloc(guac_client* client); + +/** + * Frees the given cursor. + * + * @param cursor + * The cursor to free. + */ +void guac_common_cursor_free(guac_common_cursor* cursor); + +/** + * Sends the current state of this cursor across the given socket, including + * the current cursor image. The resulting cursor on the remote display will + * be visible. + * + * @param cursor + * The cursor to send. + * + * @param client + * The user receiving the updated cursor. + * + * @param socket + * The socket over which the updated cursor should be sent. + */ +void guac_common_cursor_dup( + guac_common_cursor* cursor, guac_client* client, guac_socket* socket); + +/** + * Updates the current position and button state of the mouse cursor, marking + * the given user as the most recent user of the mouse. The remote mouse cursor + * will be hidden for this user and shown for all others. + * + * @param cursor + * The cursor being updated. + * + * @param user + * The user that moved the cursor. + * + * @param x + * The new X coordinate of the cursor. + * + * @param y + * The new Y coordinate of the cursor. + * + * @param button_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_common_cursor_update(guac_common_cursor* cursor, guac_user* user, + int x, int y, int button_mask); + +/** + * Sets the cursor image to the given raw image data. This raw image data must + * be in 32-bit ARGB format, having 8 bits per color component, where the + * alpha component is stored in the high-order 8 bits, and blue is stored + * in the low-order 8 bits. + * + * @param cursor + * The cursor to set the image of. + * + * @param hx + * The X coordinate of the hotspot of the new cursor image. + * + * @param hy + * The Y coordinate of the hotspot of the new cursor image. + * + * @param data + * A pointer to raw 32-bit ARGB image data. + * + * @param width + * The width of the given image data, in pixels. + * + * @param height + * The height of the given image data, in pixels. + * + * @param stride + * The number of bytes in a single row of image data. + */ +void guac_common_cursor_set_argb(guac_common_cursor* cursor, int hx, int hy, + unsigned const char* data, int width, int height, int stride); + +/** + * Sets the cursor image to the contents of the given surface. The entire + * contents of the surface are used, and the dimensions of the resulting + * cursor will be the dimensions of the given surface. + * + * @param cursor + * The cursor to set the image of. + * + * @param hx + * The X coordinate of the hotspot of the new cursor image. + * + * @param hy + * The Y coordinate of the hotspot of the new cursor image. + * + * @param surface + * The surface containing the cursor image. + */ +void guac_common_cursor_set_surface(guac_common_cursor* cursor, int hx, int hy, + guac_common_surface* surface); + +/** + * Set the cursor of the remote display to the embedded "pointer" graphic. The + * pointer graphic is a black arrow with white border. + * + * @param cursor + * The cursor to set the image of. + */ +void guac_common_cursor_set_pointer(guac_common_cursor* cursor); + +/** + * Set the cursor of the remote display to the embedded "dot" graphic. The dot + * graphic is a small black square with white border. + * + * @param cursor + * The cursor to set the image of. + */ +void guac_common_cursor_set_dot(guac_common_cursor* cursor); + +/** + * Sets the cursor of the remote display to the embedded "I-bar" graphic. The + * I-bar graphic is a small black "I" shape with white border, used to indicate + * the presence of selectable or editable text. + * + * @param cursor + * The cursor to set the image of. + */ +void guac_common_cursor_set_ibar(guac_common_cursor* cursor); + +/** + * Sets the cursor of the remote display to the embedded transparent (blank) + * graphic, effectively hiding the mouse cursor. + * + * @param cursor + * The cursor to set the image of. + */ +void guac_common_cursor_set_blank(guac_common_cursor* cursor); + +/** + * Removes the given user, such that future synchronization will not occur. + * This is necessary when a user leaves the connection. If a user leaves the + * connection and this is not called, the mouse cursor state may not update + * correctly in response to mouse events. + * + * @param cursor + * The cursor to remove the user from. + * + * @param user + * The user to remove. + */ +void guac_common_cursor_remove_user(guac_common_cursor* cursor, + guac_user* user); + +#endif diff --git a/src/common/common/dot_cursor.h b/src/common/common/dot_cursor.h new file mode 100644 index 000000000..c5b73880e --- /dev/null +++ b/src/common/common/dot_cursor.h @@ -0,0 +1,60 @@ +/* + * 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_DOT_CURSOR_H +#define _GUAC_COMMON_DOT_CURSOR_H + +#include "config.h" + +#include +#include + +/** + * Width of the embedded mouse cursor graphic. + */ +extern const int guac_common_dot_cursor_width; + +/** + * Height of the embedded mouse cursor graphic. + */ +extern const int guac_common_dot_cursor_height; + +/** + * Number of bytes in each row of the embedded mouse cursor graphic. + */ +extern const int guac_common_dot_cursor_stride; + +/** + * The Cairo graphic format of the mouse cursor graphic. + */ +extern const cairo_format_t guac_common_dot_cursor_format; + +/** + * Embedded mouse cursor graphic. + */ +extern unsigned char guac_common_dot_cursor[]; + +/** + * Set the cursor of the remote display to the embedded cursor graphic. + * + * @param user The guac_user to send the cursor to. + */ +void guac_common_set_dot_cursor(guac_user* user); + +#endif diff --git a/src/common/common/ibar_cursor.h b/src/common/common/ibar_cursor.h new file mode 100644 index 000000000..ae11fff7c --- /dev/null +++ b/src/common/common/ibar_cursor.h @@ -0,0 +1,62 @@ +/* + * 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_IBAR_CURSOR_H +#define GUAC_COMMON_IBAR_CURSOR_H + +#include "config.h" + +#include +#include + +/** + * Width of the embedded I-bar mouse cursor graphic. + */ +extern const int guac_common_ibar_cursor_width; + +/** + * Height of the embedded I-bar mouse cursor graphic. + */ +extern const int guac_common_ibar_cursor_height; + +/** + * Number of bytes in each row of the embedded I-bar mouse cursor graphic. + */ +extern const int guac_common_ibar_cursor_stride; + +/** + * The Cairo grapic format of the I-bar mouse cursor graphic. + */ +extern const cairo_format_t guac_common_ibar_cursor_format; + +/** + * Embedded I-bar mouse cursor graphic. + */ +extern unsigned char guac_common_ibar_cursor[]; + +/** + * Sets the cursor of the remote display to the embedded I-bar cursor graphic. + * + * @param user + * The guac_user to send the cursor to. + */ +void guac_common_set_ibar_cursor(guac_user* user); + +#endif + diff --git a/src/common/common/pointer_cursor.h b/src/common/common/pointer_cursor.h new file mode 100644 index 000000000..74559ba33 --- /dev/null +++ b/src/common/common/pointer_cursor.h @@ -0,0 +1,60 @@ +/* + * 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_POINTER_CURSOR_H +#define _GUAC_COMMON_POINTER_CURSOR_H + +#include "config.h" + +#include +#include + +/** + * Width of the embedded mouse cursor graphic. + */ +extern const int guac_common_pointer_cursor_width; + +/** + * Height of the embedded mouse cursor graphic. + */ +extern const int guac_common_pointer_cursor_height; + +/** + * Number of bytes in each row of the embedded mouse cursor graphic. + */ +extern const int guac_common_pointer_cursor_stride; + +/** + * The Cairo grapic format of the mouse cursor graphic. + */ +extern const cairo_format_t guac_common_pointer_cursor_format; + +/** + * Embedded mouse cursor graphic. + */ +extern unsigned char guac_common_pointer_cursor[]; + +/** + * Set the cursor of the remote display to the embedded cursor graphic. + * + * @param user The guac_user to send the cursor to. + */ +void guac_common_set_pointer_cursor(guac_user* user); + +#endif diff --git a/src/common/common/rect.h b/src/common/common/rect.h new file mode 100644 index 000000000..6b3104865 --- /dev/null +++ b/src/common/common/rect.h @@ -0,0 +1,143 @@ +/* + * 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_RECT_H +#define __GUAC_COMMON_RECT_H + +#include "config.h" + +/** + * Simple representation of a rectangle, having a defined corner and dimensions. + */ +typedef struct guac_common_rect { + + /** + * The X coordinate of the upper-left corner of this rectangle. + */ + int x; + + /** + * The Y coordinate of the upper-left corner of this rectangle. + */ + int y; + + /** + * The width of this rectangle. + */ + int width; + + /** + * The height of this rectangle. + */ + int height; + +} guac_common_rect; + +/** + * Initialize the given rect with the given coordinates and dimensions. + * + * @param rect The rect to initialize. + * @param x The X coordinate of the upper-left corner of the rect. + * @param y The Y coordinate of the upper-left corner of the rect. + * @param width The width of the rect. + * @param height The height of the rect. + */ +void guac_common_rect_init(guac_common_rect* rect, int x, int y, int width, int height); + +/** + * Expand the rectangle to fit an NxN grid. + * + * The rectangle will be shifted to the left and up, expanded and adjusted to + * fit within the max bounding rect. + * + * @param cell_size + * The (NxN) grid cell size. + * + * @param rect + * The rectangle to adjust. + * + * @param max_rect + * The bounding area in which the given rect can exist. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_common_rect_expand_to_grid(int cell_size, guac_common_rect* rect, + const guac_common_rect* max_rect); + +/** + * Extend the given rect such that it contains at least the specified minimum + * rect. + * + * @param rect The rect to extend. + * @param min The minimum area which must be contained within the given rect. + */ +void guac_common_rect_extend(guac_common_rect* rect, const guac_common_rect* min); + +/** + * Collapse the given rect such that it exists only within the given maximum + * rect. + * + * @param rect The rect to extend. + * @param max The maximum area in which the given rect can exist. + */ +void guac_common_rect_constrain(guac_common_rect* rect, const guac_common_rect* max); + +/** + * Check whether a rectangle intersects another. + * + * @param rect + * Rectangle to check for intersection. + * + * @param other + * The other rectangle. + * + * @return + * Zero if no intersection, 1 if partial intersection, + * 2 if first rect is completely inside the other. + */ +int guac_common_rect_intersects(const guac_common_rect* rect, + const guac_common_rect* other); + +/** + * Clip and split a rectangle into rectangles which are not covered by the + * hole rectangle. + * + * This function will clip and split single edges when executed and must be + * invoked until it returns zero. The edges are handled counter-clockwise + * starting at the top edge. + * + * @param rect + * The rectangle to be split. This rectangle will be clipped by the + * split_rect. + * + * @param hole + * The rectangle which represents the hole. + * + * @param split_rect + * Resulting split rectangle. + * + * @return + * Zero when no splits were done, non-zero when the rectangle was split. + */ +int guac_common_rect_clip_and_split(guac_common_rect* rect, + const guac_common_rect* hole, guac_common_rect* split_rect); + +#endif + diff --git a/src/common/common/surface.h b/src/common/common/surface.h new file mode 100644 index 000000000..21859a3e1 --- /dev/null +++ b/src/common/common/surface.h @@ -0,0 +1,539 @@ +/* + * 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_SURFACE_H +#define __GUAC_COMMON_SURFACE_H + +#include "config.h" +#include "rect.h" + +#include +#include +#include +#include +#include + +#include + +/** + * The maximum number of updates to allow within the bitmap queue. + */ +#define GUAC_COMMON_SURFACE_QUEUE_SIZE 256 + +/** + * Heat map cell size in pixels. Each side of each heat map cell will consist + * of this many pixels. + */ +#define GUAC_COMMON_SURFACE_HEAT_CELL_SIZE 64 + +/** + * The width or height of the heat map (in cells) given the width or height of + * the image (in pixels). + */ +#define GUAC_COMMON_SURFACE_HEAT_DIMENSION(x) ( \ + (x + GUAC_COMMON_SURFACE_HEAT_CELL_SIZE - 1) \ + / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE \ +) + +/** + * The number of entries to collect within each heat map cell. Collected + * history entries are used to determine the framerate of the region associated + * with that cell. + */ +#define GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE 5 + +/** + * Representation of a cell in the refresh heat map. This cell is used to keep + * track of how often an area on a surface is refreshed. + */ +typedef struct guac_common_surface_heat_cell { + + /** + * Timestamps of each of the last N updates covering the location + * associated with this heat map cell. This is used to calculate the + * framerate. This array is structured as a ring buffer containing history + * entries in chronologically-ascending order, starting at the entry + * pointed to by oldest_entry and proceeding through all other entries, + * wrapping around if the end of the array is reached. + */ + guac_timestamp history[GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE]; + + /** + * Index of the oldest entry within the history. + */ + int oldest_entry; + +} guac_common_surface_heat_cell; + +/** + * Representation of a bitmap update, having a rectangle of image data (stored + * elsewhere) and a flushed/not-flushed state. + */ +typedef struct guac_common_surface_bitmap_rect { + + /** + * Whether this rectangle has been flushed. + */ + int flushed; + + /** + * The rectangle containing the bitmap update. + */ + guac_common_rect rect; + +} guac_common_surface_bitmap_rect; + +/** + * Surface which backs a Guacamole buffer or layer, automatically + * combining updates when possible. + */ +typedef struct guac_common_surface { + + /** + * The layer this surface will draw to. + */ + const guac_layer* layer; + + /** + * The client associated with this surface. + */ + guac_client* client; + + /** + * The socket to send instructions on when flushing. + */ + guac_socket* socket; + + /** + * 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 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 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 width of this layer, in pixels. + */ + int width; + + /** + * The height of this layer, in pixels. + */ + int height; + + /** + * The size of each image row, in bytes. + */ + int stride; + + /** + * The underlying buffer of the Cairo surface. + */ + unsigned char* buffer; + + /** + * Non-zero if the location or parent layer of this surface has been + * changed and needs to be flushed, 0 otherwise. + */ + int location_dirty; + + /** + * Non-zero if the opacity of this surface has been changed and needs to be + * flushed, 0 otherwise. + */ + int opacity_dirty; + + /** + * Non-zero if this surface is dirty and needs to be flushed, 0 otherwise. + */ + int dirty; + + /** + * The dirty rectangle. + */ + guac_common_rect dirty_rect; + + /** + * Whether the surface actually exists on the client. + */ + int realized; + + /** + * Whether drawing operations are currently clipped by the clipping + * rectangle. + */ + int clipped; + + /** + * The clipping rectangle. + */ + guac_common_rect clip_rect; + + /** + * The number of updates in the bitmap queue. + */ + int bitmap_queue_length; + + /** + * All queued bitmap updates. + */ + guac_common_surface_bitmap_rect bitmap_queue[GUAC_COMMON_SURFACE_QUEUE_SIZE]; + + /** + * A heat map keeping track of the refresh frequency of + * the areas of the screen. + */ + guac_common_surface_heat_cell* heat_map; + + /** + * Mutex which is locked internally when access to the surface must be + * synchronized. All public functions of guac_common_surface should be + * considered threadsafe. + */ + pthread_mutex_t _lock; + +} guac_common_surface; + +/** + * Allocates a new guac_common_surface, assigning it to the given layer. + * + * @param client + * The client associated with the given layer. + * + * @param socket + * The socket to send instructions on when flushing. + * + * @param layer + * The layer to associate with the new surface. + * + * @param w + * The width of the surface. + * + * @param h + * The height of the surface. + * + * @return + * A newly-allocated guac_common_surface. + */ +guac_common_surface* guac_common_surface_alloc(guac_client* client, + guac_socket* socket, const guac_layer* layer, int w, int h); + +/** + * Frees the given guac_common_surface. Beware that this will NOT free any + * associated layers, which must be freed manually. + * + * @param surface The surface to free. + */ +void guac_common_surface_free(guac_common_surface* surface); + + /** + * Resizes the given surface to the given size. + * + * @param surface The surface to resize. + * @param w The width of the surface. + * @param h The height of the surface. + */ +void guac_common_surface_resize(guac_common_surface* surface, int w, int h); + +/** + * Draws the given data to the given guac_common_surface. If the source surface + * is ARGB, the draw operation will be performed using the Porter-Duff "over" + * composite operator. If the source surface is RGB (no alpha channel), no + * compositing is performed and destination pixels are ignored. + * + * @param surface + * The surface to draw to. + * + * @param x + * The X coordinate of the draw location. + * + * @param y + * The Y coordinate of the draw location. + * + * @param src + * The Cairo surface to retrieve data from. + */ +void guac_common_surface_draw(guac_common_surface* surface, int x, int y, + cairo_surface_t* src); + +/** + * Paints to the given guac_common_surface using the given data as a stencil, + * filling opaque regions with the specified color, and leaving transparent + * regions untouched. + * + * @param surface The surface to draw to. + * @param x The X coordinate of the draw location. + * @param y The Y coordinate of the draw location. + * @param src The Cairo surface to retrieve data from. + * @param red The red component of the fill color. + * @param green The green component of the fill color. + * @param blue The blue component of the fill color. + */ +void guac_common_surface_paint(guac_common_surface* surface, int x, int y, cairo_surface_t* src, + int red, int green, int blue); + +/** + * Copies a rectangle of data between two surfaces. + * + * @param src The source surface. + * @param sx The X coordinate of the upper-left corner of the source rect. + * @param sy The Y coordinate of the upper-left corner of the source rect. + * @param w The width of the source rect. + * @param h The height of the source rect. + * @param dst The destination surface. + * @param dx The X coordinate of the upper-left corner of the destination rect. + * @param dy The Y coordinate of the upper-left corner of the destination rect. + */ +void guac_common_surface_copy(guac_common_surface* src, int sx, int sy, int w, int h, + guac_common_surface* dst, int dx, int dy); + +/** + * Transfers a rectangle of data between two surfaces. + * + * @param src The source surface. + * @param sx The X coordinate of the upper-left corner of the source rect. + * @param sy The Y coordinate of the upper-left corner of the source rect. + * @param w The width of the source rect. + * @param h The height of the source rect. + * @param op The transfer function. + * @param dst The destination surface. + * @param dx The X coordinate of the upper-left corner of the destination rect. + * @param dy The Y coordinate of the upper-left corner of the destination rect. + */ +void guac_common_surface_transfer(guac_common_surface* src, int sx, int sy, int w, int h, + guac_transfer_function op, guac_common_surface* dst, int dx, int dy); + +/** + * Assigns the given value to all pixels within a rectangle of the given + * surface. The color of all pixels within the rectangle, including the alpha + * component, is entirely replaced. + * + * @param surface + * The surface to draw upon. + * + * @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 w + * The width of the rectangle. + * + * @param h + * The height of the rectangle. + * + * @param red + * The red component of the color value to assign to all pixels within the + * rectangle. + * + * @param green + * The green component of the color value to assign to all pixels within + * the rectangle. + * + * @param blue + * The blue component of the color value to assign to all pixels within the + * rectangle. + * + * @param alpha + * The alpha component of the color value to assign to all pixels within + * the rectangle. + */ +void guac_common_surface_set(guac_common_surface* surface, int x, int y, + int w, int h, int red, int green, int blue, int alpha); + +/** + * Given the coordinates and dimensions of a rectangle, clips all future + * operations within that rectangle. + * + * @param surface The surface whose clipping rectangle should be changed. + * @param x The X coordinate of the upper-left corner of the bounding rectangle. + * @param y The Y coordinate of the upper-left corner of the bounding rectangle. + * @param w The width of the bounding rectangle. + * @param h The height of the bounding rectangle. + */ +void guac_common_surface_clip(guac_common_surface* surface, int x, int y, int w, int h); + +/** + * Resets the clipping rectangle, allowing drawing operations throughout the + * entire surface. + * + * @param surface The surface whose clipping rectangle should be reset. + */ +void guac_common_surface_reset_clip(guac_common_surface* surface); + +/** + * Changes the location of the surface relative to its parent layer. If the + * surface does not represent a non-default visible layer, then this function + * has no effect. + * + * @param surface + * The surface to move relative to its parent layer. + * + * @param x + * The new X coordinate for the upper-left corner of the layer, in pixels. + * + * @param y + * The new Y coordinate for the upper-left corner of the layer, in pixels. + */ +void guac_common_surface_move(guac_common_surface* surface, int x, int y); + +/** + * Changes the stacking order of the surface relative to other surfaces within + * the same parent layer. If the surface does not represent a non-default + * visible layer, then this function has no effect. + * + * @param surface + * The surface to reorder relative to sibling layers. + * + * @param z + * The new Z-order for this layer, relative to sibling layers. + */ +void guac_common_surface_stack(guac_common_surface* surface, int z); + +/** + * Changes the parent layer of ths given surface. By default, layers will be + * children of the default layer. If the surface does not represent a + * non-default visible layer, then this function has no effect. + * + * @param surface + * The surface whose parent layer should be changed. + * + * @param parent + * The layer which should be set as the new parent of the given surface. + */ +void guac_common_surface_set_parent(guac_common_surface* surface, + const guac_layer* parent); + +/** + * Sets the opacity of the surface. If the surface does not represent a + * non-default visible layer, then this function has no effect. + * + * @param surface + * The surface whose opacity should be changed. + * + * @param opacity + * The level of opacity applied to this surface, where fully opaque is 255, + * and fully transparent is 0. + */ +void guac_common_surface_set_opacity(guac_common_surface* surface, int opacity); + +/** + * Flushes the given surface, including any applicable properties, drawing any + * pending operations on the remote display. + * + * @param surface + * The surface to flush. + */ +void guac_common_surface_flush(guac_common_surface* surface); + +/** + * Duplicates the contents of the current surface to the given socket. Pending + * changes are not flushed. + * + * @param surface + * The surface to duplicate. + * + * @param client + * The client whose users are receiving the surface. + * + * @param socket + * The socket over which the surface contents should be sent. + */ +void guac_common_surface_dup(guac_common_surface* surface, + guac_client* client, guac_socket* socket); + +/** + * Declares that the given surface should receive touch events. By default, + * surfaces are assumed to not expect touch events. This value is advisory, and + * the client is not required to honor the declared level of touch support. + * Implementations are expected to safely handle or ignore any received touch + * events, regardless of the level of touch support declared. regardless of + * the level of touch support declared. + * + * @param surface + * The surface to modify. + * + * @param touches + * The number of simultaneous touches that this surface can accept, where 0 + * indicates that the surface does not support touch events at all. + */ +void guac_common_surface_set_multitouch(guac_common_surface* surface, + int touches); + +/** + * Sets the lossless compression policy of the given surface to the given + * value. By default, newly-created surfaces 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. + * + * @param surface + * The surface to modify. + * + * @param lossless + * Non-zero if all graphical updates for this surface should use lossless + * compression, 0 otherwise. + */ +void guac_common_surface_set_lossless(guac_common_surface* surface, + int lossless); + +#endif + diff --git a/src/common/cursor.c b/src/common/cursor.c new file mode 100644 index 000000000..9bb51aedf --- /dev/null +++ b/src/common/cursor.c @@ -0,0 +1,327 @@ +/* + * 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/blank_cursor.h" +#include "common/dot_cursor.h" +#include "common/cursor.h" +#include "common/ibar_cursor.h" +#include "common/pointer_cursor.h" +#include "common/surface.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Allocates a cursor as well as an image buffer where the cursor is rendered. + * + * @param client + * The client owning the cursor. + * + * @return + * The newly-allocated cursor or NULL if cursor cannot be allocated. + */ +guac_common_cursor* guac_common_cursor_alloc(guac_client* client) { + + guac_common_cursor* cursor = guac_mem_alloc(sizeof(guac_common_cursor)); + if (cursor == NULL) + return NULL; + + /* Associate cursor with client and allocate cursor buffer */ + cursor->client = client; + cursor->buffer = guac_client_alloc_buffer(client); + + /* Allocate initial image buffer */ + cursor->image_buffer_size = GUAC_COMMON_CURSOR_DEFAULT_SIZE; + cursor->image_buffer = guac_mem_alloc(cursor->image_buffer_size); + + /* No cursor image yet */ + cursor->width = 0; + cursor->height = 0; + cursor->surface = NULL; + cursor->hotspot_x = 0; + cursor->hotspot_y = 0; + + /* No user has moved the mouse yet */ + cursor->user = NULL; + cursor->timestamp = guac_timestamp_current(); + + /* Start cursor in upper-left */ + cursor->x = 0; + cursor->y = 0; + + pthread_mutex_init(&(cursor->_lock), NULL); + + return cursor; + +} + +void guac_common_cursor_free(guac_common_cursor* cursor) { + + pthread_mutex_destroy(&(cursor->_lock)); + + guac_client* client = cursor->client; + guac_layer* buffer = cursor->buffer; + cairo_surface_t* surface = cursor->surface; + + /* Free image buffer and surface */ + guac_mem_free(cursor->image_buffer); + if (surface != NULL) + cairo_surface_destroy(surface); + + /* Destroy buffer within remotely-connected client */ + guac_protocol_send_dispose(client->socket, buffer); + + /* Return buffer to pool */ + guac_client_free_buffer(client, buffer); + + guac_mem_free(cursor); + +} + +void guac_common_cursor_dup( + guac_common_cursor* cursor, guac_client* client, guac_socket* socket) { + + pthread_mutex_lock(&(cursor->_lock)); + + /* Synchronize location */ + guac_protocol_send_mouse(socket, cursor->x, cursor->y, cursor->button_mask, + cursor->timestamp); + + /* Synchronize cursor image */ + if (cursor->surface != NULL) { + guac_protocol_send_size(socket, cursor->buffer, + cursor->width, cursor->height); + + guac_client_stream_png(client, socket, GUAC_COMP_SRC, + cursor->buffer, 0, 0, cursor->surface); + + guac_protocol_send_cursor(socket, + cursor->hotspot_x, cursor->hotspot_y, + cursor->buffer, 0, 0, cursor->width, cursor->height); + } + + pthread_mutex_unlock(&(cursor->_lock)); + + guac_socket_flush(socket); + +} + +/** + * 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_common_cursor whose state should be broadcast to + * all users except the user that moved the cursor last. + * + * @return + * Always NULL. + */ +static void* guac_common_cursor_broadcast_state(guac_user* user, + void* data) { + + guac_common_cursor* cursor = (guac_common_cursor*) data; + + /* Send cursor state only if the user is not moving the cursor */ + if (user != cursor->user) { + guac_protocol_send_mouse(user->socket, cursor->x, cursor->y, + cursor->button_mask, cursor->timestamp); + guac_socket_flush(user->socket); + } + + return NULL; + +} + +void guac_common_cursor_update(guac_common_cursor* cursor, guac_user* user, + int x, int y, int button_mask) { + + pthread_mutex_lock(&(cursor->_lock)); + + /* Update current user of cursor */ + cursor->user = user; + + /* Update cursor state */ + cursor->x = x; + cursor->y = y; + cursor->button_mask = button_mask; + + /* Store time at which cursor was updated */ + cursor->timestamp = guac_timestamp_current(); + + /* Notify all other users of change in cursor state */ + guac_client_foreach_user(cursor->client, + guac_common_cursor_broadcast_state, cursor); + + pthread_mutex_unlock(&(cursor->_lock)); + +} + +/** + * Ensures the cursor image buffer has enough room to fit an image with the + * given characteristics. Existing image buffer data may be destroyed. + * + * @param cursor + * The cursor whose buffer size should be checked. If this cursor lacks + * sufficient space to contain a cursor image of the specified width, + * height, and stride, the current contents of this cursor will be + * destroyed and replaced with an new buffer having sufficient space. + * + * @param width + * The required cursor width, in pixels. + * + * @param height + * The required cursor height, in pixels. + * + * @param stride + * The number of bytes in each row of image data. + */ +static void guac_common_cursor_resize(guac_common_cursor* cursor, + int width, int height, int stride) { + + size_t minimum_size = guac_mem_ckd_mul_or_die(height, stride); + + /* Grow image buffer if necessary */ + if (cursor->image_buffer_size < minimum_size) { + + /* Calculate new size */ + cursor->image_buffer_size = guac_mem_ckd_mul_or_die(minimum_size, 2); + + /* Destructively reallocate image buffer */ + guac_mem_free(cursor->image_buffer); + cursor->image_buffer = guac_mem_alloc(cursor->image_buffer_size); + + } + +} + +void guac_common_cursor_set_argb(guac_common_cursor* cursor, int hx, int hy, + unsigned const char* data, int width, int height, int stride) { + + pthread_mutex_lock(&(cursor->_lock)); + + /* Copy image data */ + guac_common_cursor_resize(cursor, width, height, stride); + memcpy(cursor->image_buffer, data, height * stride); + + if (cursor->surface != NULL) + cairo_surface_destroy(cursor->surface); + + cursor->surface = cairo_image_surface_create_for_data(cursor->image_buffer, + CAIRO_FORMAT_ARGB32, width, height, stride); + + /* Set new cursor parameters */ + cursor->width = width; + cursor->height = height; + cursor->hotspot_x = hx; + cursor->hotspot_y = hy; + + /* Broadcast new cursor image to all users */ + guac_protocol_send_size(cursor->client->socket, cursor->buffer, + width, height); + + guac_client_stream_png(cursor->client, cursor->client->socket, + GUAC_COMP_SRC, cursor->buffer, 0, 0, cursor->surface); + + /* Update cursor image */ + guac_protocol_send_cursor(cursor->client->socket, + cursor->hotspot_x, cursor->hotspot_y, + cursor->buffer, 0, 0, cursor->width, cursor->height); + + guac_socket_flush(cursor->client->socket); + + pthread_mutex_unlock(&(cursor->_lock)); + +} + +void guac_common_cursor_set_surface(guac_common_cursor* cursor, int hx, int hy, + guac_common_surface* surface) { + + /* Set cursor to surface contents */ + guac_common_cursor_set_argb(cursor, hx, hy, surface->buffer, + surface->width, surface->height, surface->stride); + +} + +void guac_common_cursor_set_pointer(guac_common_cursor* cursor) { + + guac_common_cursor_set_argb(cursor, 0, 0, + guac_common_pointer_cursor, + guac_common_pointer_cursor_width, + guac_common_pointer_cursor_height, + guac_common_pointer_cursor_stride); + +} + +void guac_common_cursor_set_dot(guac_common_cursor* cursor) { + + guac_common_cursor_set_argb(cursor, 2, 2, + guac_common_dot_cursor, + guac_common_dot_cursor_width, + guac_common_dot_cursor_height, + guac_common_dot_cursor_stride); + +} + +void guac_common_cursor_set_ibar(guac_common_cursor* cursor) { + + guac_common_cursor_set_argb(cursor, + guac_common_ibar_cursor_width / 2, + guac_common_ibar_cursor_height / 2, + guac_common_ibar_cursor, + guac_common_ibar_cursor_width, + guac_common_ibar_cursor_height, + guac_common_ibar_cursor_stride); + +} + +void guac_common_cursor_set_blank(guac_common_cursor* cursor) { + + guac_common_cursor_set_argb(cursor, 0, 0, + guac_common_blank_cursor, + guac_common_blank_cursor_width, + guac_common_blank_cursor_height, + guac_common_blank_cursor_stride); + +} + +void guac_common_cursor_remove_user(guac_common_cursor* cursor, + guac_user* user) { + + pthread_mutex_lock(&(cursor->_lock)); + + /* Disassociate from given user */ + if (cursor->user == user) + cursor->user = NULL; + + pthread_mutex_unlock(&(cursor->_lock)); + +} + diff --git a/src/common/dot_cursor.c b/src/common/dot_cursor.c new file mode 100644 index 000000000..fde79b01a --- /dev/null +++ b/src/common/dot_cursor.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 "config.h" + +#include +#include +#include +#include +#include +#include + +/* Macros for prettying up the embedded image. */ +#define X 0x00,0x00,0x00,0xFF +#define O 0xFF,0xFF,0xFF,0xFF +#define _ 0x00,0x00,0x00,0x00 + +/* Dimensions */ +const int guac_common_dot_cursor_width = 5; +const int guac_common_dot_cursor_height = 5; + +/* Format */ +const cairo_format_t guac_common_dot_cursor_format = CAIRO_FORMAT_ARGB32; +const int guac_common_dot_cursor_stride = 20; + +/* Embedded pointer graphic */ +unsigned char guac_common_dot_cursor[] = { + + _,O,O,O,_, + O,X,X,X,O, + O,X,X,X,O, + O,X,X,X,O, + _,O,O,O,_ + +}; + +void guac_common_set_dot_cursor(guac_user* user) { + + guac_client* client = user->client; + guac_socket* socket = user->socket; + + /* Draw to buffer */ + guac_layer* cursor = guac_client_alloc_buffer(client); + + cairo_surface_t* graphic = cairo_image_surface_create_for_data( + guac_common_dot_cursor, + guac_common_dot_cursor_format, + guac_common_dot_cursor_width, + guac_common_dot_cursor_height, + guac_common_dot_cursor_stride); + + guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, + 0, 0, graphic); + cairo_surface_destroy(graphic); + + /* Set cursor */ + guac_protocol_send_cursor(socket, 2, 2, cursor, + 0, 0, + guac_common_dot_cursor_width, + guac_common_dot_cursor_height); + + /* Free buffer */ + guac_client_free_buffer(client, cursor); + + guac_client_log(client, GUAC_LOG_DEBUG, + "Client cursor image set to generic built-in dot."); + +} + diff --git a/src/common/ibar_cursor.c b/src/common/ibar_cursor.c new file mode 100644 index 000000000..6494591bc --- /dev/null +++ b/src/common/ibar_cursor.c @@ -0,0 +1,98 @@ +/* + * 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 +#include +#include +#include +#include +#include + +/* Macros for prettying up the embedded image. */ +#define X 0x00,0x00,0x00,0xFF +#define U 0x80,0x80,0x80,0xFF +#define O 0xFF,0xFF,0xFF,0xFF +#define _ 0x00,0x00,0x00,0x00 + +/* Dimensions */ +const int guac_common_ibar_cursor_width = 7; +const int guac_common_ibar_cursor_height = 16; + +/* Format */ +const cairo_format_t guac_common_ibar_cursor_format = CAIRO_FORMAT_ARGB32; +const int guac_common_ibar_cursor_stride = 28; + +/* Embedded I-bar graphic */ +unsigned char guac_common_ibar_cursor[] = { + + 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 + +}; + +void guac_common_set_ibar_cursor(guac_user* user) { + + guac_client* client = user->client; + guac_socket* socket = user->socket; + + /* Draw to buffer */ + guac_layer* cursor = guac_client_alloc_buffer(client); + + cairo_surface_t* graphic = cairo_image_surface_create_for_data( + guac_common_ibar_cursor, + guac_common_ibar_cursor_format, + guac_common_ibar_cursor_width, + guac_common_ibar_cursor_height, + guac_common_ibar_cursor_stride); + + guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, + 0, 0, graphic); + cairo_surface_destroy(graphic); + + /* Set cursor */ + guac_protocol_send_cursor(socket, 0, 0, cursor, + guac_common_ibar_cursor_width / 2, + guac_common_ibar_cursor_height / 2, + guac_common_ibar_cursor_width, + guac_common_ibar_cursor_height); + + /* Free buffer */ + guac_client_free_buffer(client, cursor); + + guac_client_log(client, GUAC_LOG_DEBUG, + "Client cursor image set to generic built-in I-bar."); + +} + diff --git a/src/common/pointer_cursor.c b/src/common/pointer_cursor.c new file mode 100644 index 000000000..fbcb60230 --- /dev/null +++ b/src/common/pointer_cursor.c @@ -0,0 +1,96 @@ +/* + * 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 +#include +#include +#include +#include +#include + +/* Macros for prettying up the embedded image. */ +#define X 0x00,0x00,0x00,0xFF +#define O 0xFF,0xFF,0xFF,0xFF +#define _ 0x00,0x00,0x00,0x00 + +/* Dimensions */ +const int guac_common_pointer_cursor_width = 11; +const int guac_common_pointer_cursor_height = 16; + +/* Format */ +const cairo_format_t guac_common_pointer_cursor_format = CAIRO_FORMAT_ARGB32; +const int guac_common_pointer_cursor_stride = 44; + +/* Embedded pointer graphic */ +unsigned char guac_common_pointer_cursor[] = { + + 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,_,_ + +}; + +void guac_common_set_pointer_cursor(guac_user* user) { + + guac_client* client = user->client; + guac_socket* socket = user->socket; + + /* Draw to buffer */ + guac_layer* cursor = guac_client_alloc_buffer(client); + + cairo_surface_t* graphic = cairo_image_surface_create_for_data( + guac_common_pointer_cursor, + guac_common_pointer_cursor_format, + guac_common_pointer_cursor_width, + guac_common_pointer_cursor_height, + guac_common_pointer_cursor_stride); + + guac_user_stream_png(user, socket, GUAC_COMP_SRC, cursor, + 0, 0, graphic); + cairo_surface_destroy(graphic); + + /* Set cursor */ + guac_protocol_send_cursor(socket, 0, 0, cursor, + 0, 0, + guac_common_pointer_cursor_width, + guac_common_pointer_cursor_height); + + /* Free buffer */ + guac_client_free_buffer(client, cursor); + + guac_client_log(client, GUAC_LOG_DEBUG, + "Client cursor image set to generic built-in pointer."); + +} + diff --git a/src/common/rect.c b/src/common/rect.c new file mode 100644 index 000000000..a7ca7da52 --- /dev/null +++ b/src/common/rect.c @@ -0,0 +1,266 @@ +/* + * 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 "common/rect.h" + +void guac_common_rect_init(guac_common_rect* rect, int x, int y, int width, int height) { + rect->x = x; + rect->y = y; + rect->width = width; + rect->height = height; +} + +void guac_common_rect_extend(guac_common_rect* rect, const guac_common_rect* min) { + + /* Calculate extents of existing dirty rect */ + int left = rect->x; + int top = rect->y; + int right = left + rect->width; + int bottom = top + rect->height; + + /* Calculate missing extents of given new rect */ + int min_left = min->x; + int min_top = min->y; + int min_right = min_left + min->width; + int min_bottom = min_top + min->height; + + /* Update minimums */ + if (min_left < left) left = min_left; + if (min_top < top) top = min_top; + if (min_right > right) right = min_right; + if (min_bottom > bottom) bottom = min_bottom; + + /* Commit rect */ + guac_common_rect_init(rect, left, top, right - left, bottom - top); + +} + +void guac_common_rect_constrain(guac_common_rect* rect, const guac_common_rect* max) { + + /* Calculate extents of existing dirty rect */ + int left = rect->x; + int top = rect->y; + int right = left + rect->width; + int bottom = top + rect->height; + + /* Calculate missing extents of given new rect */ + int max_left = max->x; + int max_top = max->y; + int max_right = max_left + max->width; + int max_bottom = max_top + max->height; + + /* Update maximums */ + if (max_left > left) left = max_left; + if (max_top > top) top = max_top; + if (max_right < right) right = max_right; + if (max_bottom < bottom) bottom = max_bottom; + + /* Commit rect */ + guac_common_rect_init(rect, left, top, right - left, bottom - top); + +} + +int guac_common_rect_expand_to_grid(int cell_size, guac_common_rect* rect, + const guac_common_rect* max_rect) { + + /* Invalid cell_size received */ + if (cell_size <= 0) + return -1; + + /* Nothing to do */ + if (cell_size == 1) + return 0; + + /* Calculate how much the rectangle must be adjusted to fit within the + * given cell size. */ + int dw = cell_size - rect->width % cell_size; + int dh = cell_size - rect->height % cell_size; + + int dx = dw / 2; + int dy = dh / 2; + + /* Set initial extents of adjusted rectangle. */ + int top = rect->y - dy; + int left = rect->x - dx; + int bottom = top + rect->height + dh; + int right = left + rect->width + dw; + + /* The max rectangle */ + int max_left = max_rect->x; + int max_top = max_rect->y; + int max_right = max_left + max_rect->width; + int max_bottom = max_top + max_rect->height; + + /* If the adjusted rectangle has sides beyond the max rectangle, or is larger + * in any direction; shift or adjust the rectangle while trying to fit in + * the grid */ + + /* Adjust left/right */ + if (right > max_right) { + + /* shift to left */ + dw = right - max_right; + right -= dw; + left -= dw; + + /* clamp left if too far */ + if (left < max_left) { + left = max_left; + } + } + else if (left < max_left) { + + /* shift to right */ + dw = max_left - left; + left += dw; + right += dw; + + /* clamp right if too far */ + if (right > max_right) { + right = max_right; + } + } + + /* Adjust top/bottom */ + if (bottom > max_bottom) { + + /* shift up */ + dh = bottom - max_bottom; + bottom -= dh; + top -= dh; + + /* clamp top if too far */ + if (top < max_top) { + top = max_top; + } + } + else if (top < max_top) { + + /* shift down */ + dh = max_top - top; + top += dh; + bottom += dh; + + /* clamp bottom if too far */ + if (bottom > max_bottom) { + bottom = max_bottom; + } + } + + /* Commit rect */ + guac_common_rect_init(rect, left, top, right - left, bottom - top); + + return 0; + +} + +int guac_common_rect_intersects(const guac_common_rect* rect, + const guac_common_rect* other) { + + /* Empty (no intersection) */ + if (other->x + other->width < rect->x || rect->x + rect->width < other->x || + other->y + other->height < rect->y || rect->y + rect->height < other->y) { + return 0; + } + /* Complete */ + else if (other->x <= rect->x && (other->x + other->width) >= (rect->x + rect->width) && + other->y <= rect->y && (other->y + other->height) >= (rect->y + rect->height)) { + return 2; + } + /* Partial intersection */ + return 1; + +} + +int guac_common_rect_clip_and_split(guac_common_rect* rect, + const guac_common_rect* hole, guac_common_rect* split_rect) { + + /* Only continue if the rectangles intersects */ + if (!guac_common_rect_intersects(rect, hole)) + return 0; + + int top, left, bottom, right; + + /* Clip and split top */ + if (rect->y < hole->y) { + top = rect->y; + left = rect->x; + bottom = hole->y; + right = rect->x + rect->width; + guac_common_rect_init(split_rect, left, top, right - left, bottom - top); + + /* Re-initialize original rect */ + top = hole->y; + bottom = rect->y + rect->height; + guac_common_rect_init(rect, left, top, right - left, bottom - top); + + return 1; + } + + /* Clip and split left */ + else if (rect->x < hole->x) { + top = rect->y; + left = rect->x; + bottom = rect->y + rect->height; + right = hole->x; + guac_common_rect_init(split_rect, left, top, right - left, bottom - top); + + /* Re-initialize original rect */ + left = hole->x; + right = rect->x + rect->width; + guac_common_rect_init(rect, left, top, right - left, bottom - top); + + return 1; + } + + /* Clip and split bottom */ + else if (rect->y + rect->height > hole->y + hole->height) { + top = hole->y + hole->height; + left = rect->x; + bottom = rect->y + rect->height; + right = rect->x + rect->width; + guac_common_rect_init(split_rect, left, top, right - left, bottom - top); + + /* Re-initialize original rect */ + top = rect->y; + bottom = hole->y + hole->height; + guac_common_rect_init(rect, left, top, right - left, bottom - top); + + return 1; + } + + /* Clip and split right */ + else if (rect->x + rect->width > hole->x + hole->width) { + top = rect->y; + left = hole->x + hole->width; + bottom = rect->y + rect->height; + right = rect->x + rect->width; + guac_common_rect_init(split_rect, left, top, right - left, bottom - top); + + /* Re-initialize original rect */ + left = rect->x; + right = hole->x + hole->width; + guac_common_rect_init(rect, left, top, right - left, bottom - top); + + return 1; + } + + return 0; +} diff --git a/src/common/surface.c b/src/common/surface.c new file mode 100644 index 000000000..3ec5517e9 --- /dev/null +++ b/src/common/surface.c @@ -0,0 +1,2041 @@ +/* + * 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 "common/rect.h" +#include "common/surface.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * The width of an update which should be considered negible and thus + * trivial overhead compared ot the cost of two updates. + */ +#define GUAC_SURFACE_NEGLIGIBLE_WIDTH 64 + +/** + * The height of an update which should be considered negible and thus + * trivial overhead compared ot the cost of two updates. + */ +#define GUAC_SURFACE_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_SURFACE_DATA_FACTOR 16 + +/** + * 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_SURFACE_BASE_COST 4096 + +/** + * An increase in cost is negligible if it is less than + * 1/GUAC_SURFACE_NEGLIGIBLE_INCREASE of the old cost. + */ +#define GUAC_SURFACE_NEGLIGIBLE_INCREASE 4 + +/** + * If combining an update because it appears to be follow a fill pattern, + * the combined cost must not exceed + * GUAC_SURFACE_FILL_PATTERN_FACTOR * (total uncombined cost). + */ +#define GUAC_SURFACE_FILL_PATTERN_FACTOR 3 + +/* 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 + +/** + * The framerate which, if exceeded, indicates that JPEG is preferred. + */ +#define GUAC_COMMON_SURFACE_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_SURFACE_JPEG_MIN_BITMAP_SIZE 4096 + +/** + * The JPEG compression min block size. This defines the optimal rectangle block + * size factor for JPEG compression. Usually 8x8 would suffice, but use 16 to + * reduce the occurrence of ringing artifacts further. + */ +#define GUAC_SURFACE_JPEG_BLOCK_SIZE 16 + +/** + * The WebP compression min block size. 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 8 + +void guac_common_surface_set_multitouch(guac_common_surface* surface, + int touches) { + + pthread_mutex_lock(&surface->_lock); + + surface->touches = touches; + guac_protocol_send_set_int(surface->socket, surface->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, touches); + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_set_lossless(guac_common_surface* surface, + int lossless) { + + pthread_mutex_lock(&surface->_lock); + surface->lossless = lossless; + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_move(guac_common_surface* surface, int x, int y) { + + pthread_mutex_lock(&surface->_lock); + + surface->x = x; + surface->y = y; + surface->location_dirty = 1; + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_stack(guac_common_surface* surface, int z) { + + pthread_mutex_lock(&surface->_lock); + + surface->z = z; + surface->location_dirty = 1; + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_set_parent(guac_common_surface* surface, + const guac_layer* parent) { + + pthread_mutex_lock(&surface->_lock); + + surface->parent = parent; + surface->location_dirty = 1; + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_set_opacity(guac_common_surface* surface, + int opacity) { + + pthread_mutex_lock(&surface->_lock); + + surface->opacity = opacity; + surface->opacity_dirty = 1; + + pthread_mutex_unlock(&surface->_lock); + +} + +/** + * Updates the coordinates of the given rectangle to be within the bounds of + * the given surface. + * + * @param surface The surface to use for clipping. + * @param rect The rectangle to clip. + * @param sx The X coordinate of the source rectangle, if any. + * @param sy The Y coordinate of the source rectangle, if any. + */ +static void __guac_common_bound_rect(guac_common_surface* surface, + guac_common_rect* rect, int* sx, int* sy) { + + guac_common_rect bounds_rect = { + .x = 0, + .y = 0, + .width = surface->width, + .height = surface->height + }; + + int orig_x = rect->x; + int orig_y = rect->y; + + guac_common_rect_constrain(rect, &bounds_rect); + + /* Update source X/Y if given */ + if (sx != NULL) *sx += rect->x - orig_x; + if (sy != NULL) *sy += rect->y - orig_y; + +} + +/** + * Updates the coordinates of the given rectangle to be within the clipping + * rectangle of the given surface, which must always be within the bounding + * rectangle of the given surface. + * + * @param surface The surface to use for clipping. + * @param rect The rectangle to clip. + * @param sx The X coordinate of the source rectangle, if any. + * @param sy The Y coordinate of the source rectangle, if any. + */ +static void __guac_common_clip_rect(guac_common_surface* surface, + guac_common_rect* rect, int* sx, int* sy) { + + int orig_x = rect->x; + int orig_y = rect->y; + + /* Just bound within surface if no clipping rectangle applied */ + if (!surface->clipped) { + __guac_common_bound_rect(surface, rect, sx, sy); + return; + } + + guac_common_rect_constrain(rect, &surface->clip_rect); + + /* Update source X/Y if given */ + if (sx != NULL) *sx += rect->x - orig_x; + if (sy != NULL) *sy += rect->y - orig_y; + +} + +/** + * Returns whether a rectangle within the given surface contains only fully + * opaque pixels. + * + * @param surface + * The surface to check. + * + * @param rect + * The rectangle to check. + * + * @return + * Non-zero if the rectangle contains only fully opaque pixels, zero + * otherwise. + */ +static int __guac_common_surface_is_opaque(guac_common_surface* surface, + guac_common_rect* rect) { + + int x, y; + + int stride = surface ->stride; + unsigned char* buffer = + surface->buffer + (stride * rect->y) + (4 * rect->x); + + /* For each row */ + for (y = 0; y < rect->height; y++) { + + /* Search for a non-opaque pixel */ + uint32_t* current = (uint32_t*) buffer; + for (x=0; x < rect->width; x++) { + + /* Rectangle is non-opaque if a single non-opaque pixel is found */ + uint32_t color = *(current++); + if ((color & 0xFF000000) != 0xFF000000) + return 0; + + } + + /* Next row */ + buffer += stride; + + } + + /* Rectangle is opaque */ + return 1; + +} + +/** + * Returns whether the given rectangle should be combined into the existing + * dirty rectangle, to be eventually flushed as image data, or would be best + * kept independent of the current rectangle. + * + * @param surface + * The surface being updated. + * + * @param rect + * The bounding rectangle of the update being made to the surface. + * + * @param rect_only + * Non-zero if this update, by its nature, contains only metainformation + * about the update's bounding rectangle, zero if the update also contains + * image data. + * + * @return + * Non-zero if the update should be combined with any existing update, zero + * otherwise. + */ +static int __guac_common_should_combine(guac_common_surface* surface, const guac_common_rect* rect, int rect_only) { + + /* Always favor combining updates if surface is currently a purely + * server-side scratch area */ + if (!surface->realized) + return 1; + + if (surface->dirty) { + + int combined_cost, dirty_cost, update_cost; + + /* Simulate combination */ + guac_common_rect combined = surface->dirty_rect; + guac_common_rect_extend(&combined, rect); + + /* Combine if result is still small */ + if (combined.width <= GUAC_SURFACE_NEGLIGIBLE_WIDTH && combined.height <= GUAC_SURFACE_NEGLIGIBLE_HEIGHT) + return 1; + + /* Estimate costs of the existing update, new update, and both combined */ + combined_cost = GUAC_SURFACE_BASE_COST + combined.width * combined.height; + dirty_cost = GUAC_SURFACE_BASE_COST + surface->dirty_rect.width * surface->dirty_rect.height; + update_cost = GUAC_SURFACE_BASE_COST + rect->width * rect->height; + + /* Reduce cost if no image data */ + if (rect_only) + update_cost /= GUAC_SURFACE_DATA_FACTOR; + + /* Combine if cost estimate shows benefit */ + if (combined_cost <= update_cost + dirty_cost) + return 1; + + /* Combine if increase in cost is negligible */ + if (combined_cost - dirty_cost <= dirty_cost / GUAC_SURFACE_NEGLIGIBLE_INCREASE) + return 1; + + if (combined_cost - update_cost <= update_cost / GUAC_SURFACE_NEGLIGIBLE_INCREASE) + return 1; + + /* Combine if we anticipate further updates, as this update follows a common fill pattern */ + if (rect->x == surface->dirty_rect.x && rect->y == surface->dirty_rect.y + surface->dirty_rect.height) { + if (combined_cost <= (dirty_cost + update_cost) * GUAC_SURFACE_FILL_PATTERN_FACTOR) + return 1; + } + + } + + /* Otherwise, do not combine */ + return 0; + +} + +/** + * Expands the dirty rect of the given surface to contain the rect described by the given + * coordinates. + * + * @param surface The surface to mark as dirty. + * @param rect The rectangle of the update which is dirtying the surface. + */ +static void __guac_common_mark_dirty(guac_common_surface* surface, const guac_common_rect* rect) { + + /* Ignore empty rects */ + if (rect->width <= 0 || rect->height <= 0) + return; + + /* If already dirty, update existing rect */ + if (surface->dirty) + guac_common_rect_extend(&surface->dirty_rect, rect); + + /* Otherwise init dirty rect */ + else { + surface->dirty_rect = *rect; + surface->dirty = 1; + } + +} + +/** + * Calculate the current average framerate for a given area on the surface. + * + * @param surface + * The surface on which the framerate will be calculated. + * + * @param rect + * The rect containing the area for which the average framerate will be + * calculated. + * + * @return + * The average framerate of the given area, in frames per second. + */ +static unsigned int __guac_common_surface_calculate_framerate( + guac_common_surface* surface, const guac_common_rect* rect) { + + int x, y; + + /* Calculate heat map dimensions */ + size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(surface->width); + + /* Calculate minimum X/Y coordinates intersecting given rect */ + int min_x = rect->x / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + int min_y = rect->y / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + + /* Calculate maximum X/Y coordinates intersecting given rect */ + int max_x = min_x + (rect->width - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + int max_y = min_y + (rect->height - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + + unsigned int sum_framerate = 0; + unsigned int count = 0; + + /* Get start of buffer at given coordinates */ + const guac_common_surface_heat_cell* heat_row = + surface->heat_map + min_y * heat_width + min_x; + + /* Iterate over all the heat map cells for the area + * and calculate the average framerate */ + for (y = min_y; y < max_y; y++) { + + /* Get current row of heat map */ + const guac_common_surface_heat_cell* heat_cell = heat_row; + + /* For each cell in subset of row */ + for (x = min_x; x < max_x; x++) { + + /* Calculate indicies for latest and oldest history entries */ + int oldest_entry = heat_cell->oldest_entry; + int latest_entry = oldest_entry - 1; + if (latest_entry < 0) + latest_entry = GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE - 1; + + /* Calculate elapsed time covering entire history for this cell */ + int elapsed_time = heat_cell->history[latest_entry] + - heat_cell->history[oldest_entry]; + + /* Calculate and add framerate */ + if (elapsed_time) + sum_framerate += GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE + * 1000 / elapsed_time; + + /* Next heat map cell */ + heat_cell++; + count++; + + } + + /* Next heat map row */ + heat_row += heat_width; + + } + + /* Calculate the average framerate over entire rect */ + if (count) + return sum_framerate / count; + + return 0; + +} + + /** + * Guesses whether a rectangle within a particular surface 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 surface + * The surface containing the image data to check. + * + * @param rect + * The rect to check within the given surface. + * + * @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 __guac_common_surface_png_optimality(guac_common_surface* surface, + const guac_common_rect* rect) { + + int x, y; + + int num_same = 0; + int num_different = 1; + + /* Get image/buffer metrics */ + int width = rect->width; + int height = rect->height; + int stride = surface->stride; + + /* Get buffer from surface */ + unsigned char* buffer = surface->buffer + rect->y * stride + rect->x * 4; + + /* Image must be at least 1x1 */ + if (width < 1 || height < 1) + return 0; + + /* For each row */ + for (y = 0; y < height; y++) { + + uint32_t* row = (uint32_t*) buffer; + uint32_t last_pixel = *(row++) | 0xFF000000; + + /* For each pixel in current row */ + for (x = 1; x < width; 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 */ + return 0x100 * num_same / num_different - 0x400; + +} + +/** + * Returns whether the given rectangle would be optimally encoded as JPEG + * rather than PNG. + * + * @param surface + * The surface to be queried. + * + * @param rect + * The rectangle to check. + * + * @return + * Non-zero if the rectangle would be optimally encoded as JPEG, zero + * otherwise. + */ +static int __guac_common_surface_should_use_jpeg(guac_common_surface* surface, + const guac_common_rect* rect) { + + /* Do not use JPEG if lossless quality is required */ + if (surface->lossless) + return 0; + + /* Calculate the average framerate for the given rect */ + int framerate = __guac_common_surface_calculate_framerate(surface, rect); + + 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_COMMON_SURFACE_JPEG_FRAMERATE + && rect_size > GUAC_SURFACE_JPEG_MIN_BITMAP_SIZE + && __guac_common_surface_png_optimality(surface, rect) < 0; + +} + +/** + * Returns whether the given rectangle would be optimally encoded as WebP + * rather than PNG. + * + * @param surface + * The surface to be queried. + * + * @param rect + * The rectangle to check. + * + * @return + * Non-zero if the rectangle would be optimally encoded as WebP, zero + * otherwise. + */ +static int __guac_common_surface_should_use_webp(guac_common_surface* surface, + const guac_common_rect* rect) { + + /* Do not use WebP if not supported */ + if (!guac_client_supports_webp(surface->client)) + return 0; + + /* Calculate the average framerate for the given rect */ + int framerate = __guac_common_surface_calculate_framerate(surface, rect); + + /* WebP is preferred if: + * - frame rate is high enough + * - PNG is not more optimal based on image contents */ + return framerate >= GUAC_COMMON_SURFACE_JPEG_FRAMERATE + && __guac_common_surface_png_optimality(surface, rect) < 0; + +} + +/** + * Updates the heat map cells which intersect the given rectangle using the + * given timestamp. This timestamp, along with timestamps from past updates, + * is used to calculate the framerate of each heat cell. + * + * @param surface + * The surface containing the heat map cells to be updated. + * + * @param rect + * The rectangle containing the heat map cells to be updated. + * + * @param time + * The timestamp to use when updating the heat map cells which intersect + * the given rectangle. + */ +static void __guac_common_surface_touch_rect(guac_common_surface* surface, + guac_common_rect* rect, guac_timestamp time) { + + int x, y; + + /* Calculate heat map dimensions */ + size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(surface->width); + + /* Calculate minimum X/Y coordinates intersecting given rect */ + int min_x = rect->x / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + int min_y = rect->y / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + + /* Calculate maximum X/Y coordinates intersecting given rect */ + int max_x = min_x + (rect->width - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + int max_y = min_y + (rect->height - 1) / GUAC_COMMON_SURFACE_HEAT_CELL_SIZE; + + /* Get start of buffer at given coordinates */ + guac_common_surface_heat_cell* heat_row = + surface->heat_map + min_y * heat_width + min_x; + + /* Update all heat map cells which intersect with rectangle */ + for (y = min_y; y <= max_y; y++) { + + /* Get current row of heat map */ + guac_common_surface_heat_cell* heat_cell = heat_row; + + /* For each cell in subset of row */ + for (x = min_x; x <= max_x; x++) { + + /* Replace oldest entry with new timestamp */ + heat_cell->history[heat_cell->oldest_entry] = time; + + /* Update to next oldest entry */ + heat_cell->oldest_entry++; + if (heat_cell->oldest_entry >= + GUAC_COMMON_SURFACE_HEAT_CELL_HISTORY_SIZE) + heat_cell->oldest_entry = 0; + + /* Advance to next heat map cell */ + heat_cell++; + + } + + /* Next heat map row */ + heat_row += heat_width; + + } + +} + +/** + * Flushes the bitmap update currently described by the dirty rectangle within the + * given surface to that surface's bitmap queue. There MUST be space within the + * queue. + * + * @param surface The surface to flush. + */ +static void __guac_common_surface_flush_to_queue(guac_common_surface* surface) { + + guac_common_surface_bitmap_rect* rect; + + /* Do not flush if not dirty */ + if (!surface->dirty) + return; + + /* Add new rect to queue */ + rect = &(surface->bitmap_queue[surface->bitmap_queue_length++]); + rect->rect = surface->dirty_rect; + rect->flushed = 0; + + /* Surface now flushed */ + surface->dirty = 0; + +} + +/** + * Flushes the given surface, drawing any pending operations on the remote + * display. Surface properties are not flushed. + * + * @param surface + * The surface to flush. + */ +static void __guac_common_surface_flush(guac_common_surface* surface); + +/** + * Schedules a deferred flush of the given surface. This will not immediately + * flush the surface to the client. Instead, the result of the flush is + * added to a queue which is reinspected and combined (if possible) with other + * deferred flushes during the call to guac_common_surface_flush(). + * + * @param surface The surface to flush. + */ +static void __guac_common_surface_flush_deferred(guac_common_surface* surface) { + + /* Do not flush if not dirty */ + if (!surface->dirty) + return; + + /* Flush if queue size has reached maximum (space is reserved for the final + * dirty rect, as __guac_common_surface_flush() MAY add an additional rect + * to the queue */ + if (surface->bitmap_queue_length == GUAC_COMMON_SURFACE_QUEUE_SIZE-1) + __guac_common_surface_flush(surface); + + /* Append dirty rect to queue */ + __guac_common_surface_flush_to_queue(surface); + +} + +/** + * Transfers a single uint32_t using the given transfer function. + * + * @param op The transfer function to use. + * @param src The source of the uint32_t value. + * @param dst THe destination which will hold the result of the transfer. + * @return Non-zero if the destination value was changed, zero otherwise. + */ +static int __guac_common_surface_transfer_int(guac_transfer_function op, uint32_t* src, uint32_t* dst) { + + uint32_t orig = *dst; + + switch (op) { + + case GUAC_TRANSFER_BINARY_BLACK: + *dst = 0xFF000000; + break; + + case GUAC_TRANSFER_BINARY_WHITE: + *dst = 0xFFFFFFFF; + break; + + case GUAC_TRANSFER_BINARY_SRC: + *dst = *src; + break; + + case GUAC_TRANSFER_BINARY_DEST: + /* NOP */ + break; + + case GUAC_TRANSFER_BINARY_NSRC: + *dst = *src ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_NDEST: + *dst = *dst ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_AND: + *dst = ((*dst) & (0xFF000000 | *src)); + break; + + case GUAC_TRANSFER_BINARY_NAND: + *dst = ((*dst) & (0xFF000000 | *src)) ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_OR: + *dst = ((*dst) | (0x00FFFFFF & *src)); + break; + + case GUAC_TRANSFER_BINARY_NOR: + *dst = ((*dst) | (0x00FFFFFF & *src)) ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_XOR: + *dst = ((*dst) ^ (0x00FFFFFF & *src)); + break; + + case GUAC_TRANSFER_BINARY_XNOR: + *dst = ((*dst) ^ (0x00FFFFFF & *src)) ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_NSRC_AND: + *dst = ((*dst) & (0xFF000000 | (*src ^ 0x00FFFFFF))); + break; + + case GUAC_TRANSFER_BINARY_NSRC_NAND: + *dst = ((*dst) & (0xFF000000 | (*src ^ 0x00FFFFFF))) ^ 0x00FFFFFF; + break; + + case GUAC_TRANSFER_BINARY_NSRC_OR: + *dst = ((*dst) | (0x00FFFFFF & (*src ^ 0x00FFFFFF))); + break; + + case GUAC_TRANSFER_BINARY_NSRC_NOR: + *dst = ((*dst) | (0x00FFFFFF & (*src ^ 0x00FFFFFF))) ^ 0x00FFFFFF; + break; + + } + + return *dst != orig; + +} + +/** + * Assigns the given value to all pixels within a rectangle of the backing + * surface of the given destination surface. The color of all pixels within the + * rectangle, including the alpha component, is entirely replaced. + * + * @param dst + * The destination surface. + * + * @param rect + * The rectangle to draw. + * + * @param red + * The red component of the color value to assign to all pixels within the + * rectangle. + * + * @param green + * The green component of the color value to assign to all pixels within + * the rectangle. + * + * @param blue + * The blue component of the color value to assign to all pixels within the + * rectangle. + * + * @param alpha + * The alpha component of the color value to assign to all pixels within + * the rectangle. + */ +static void __guac_common_surface_set(guac_common_surface* dst, + guac_common_rect* rect, int red, int green, int blue, int alpha) { + + int x, y; + + int dst_stride; + unsigned char* dst_buffer; + + uint32_t color = (alpha << 24) | (red << 16) | (green << 8) | blue; + + int min_x = rect->width - 1; + int min_y = rect->height - 1; + int max_x = 0; + int max_y = 0; + + dst_stride = dst->stride; + dst_buffer = dst->buffer + (dst_stride * rect->y) + (4 * rect->x); + + /* For each row */ + for (y=0; y < rect->height; y++) { + + uint32_t* dst_current = (uint32_t*) dst_buffer; + + /* Set row */ + for (x=0; x < rect->width; x++) { + + uint32_t old_color = *dst_current; + + if (old_color != color) { + if (x < min_x) min_x = x; + if (y < min_y) min_y = y; + if (x > max_x) max_x = x; + if (y > max_y) max_y = y; + *dst_current = color; + } + + dst_current++; + } + + /* Next row */ + dst_buffer += dst_stride; + + } + + /* Restrict destination rect to only updated pixels */ + if (max_x >= min_x && max_y >= min_y) { + rect->x += min_x; + rect->y += min_y; + rect->width = max_x - min_x + 1; + rect->height = max_y - min_y + 1; + } + else { + rect->width = 0; + rect->height = 0; + } + +} + +/** + * Applies the Porter-Duff "over" composite operator, blending the two given + * color components using the given alpha value. + * + * @param dst + * The destination color component. + * + * @param src + * The source color component. + * + * @param alpha + * The alpha value which applies to the blending operation. + * + * @return + * The result of applying the Porter-Duff "over" composite operator to the + * given source and destination components. + */ +static int guac_common_surface_blend_component(int dst, int src, int alpha) { + + int blended = src + dst * (0xFF - alpha); + + /* Do not exceed maximum component value */ + if (blended > 0xFF) + return 0xFF; + + return blended; + +} + +/** + * Applies the Porter-Duff "over" composite operator, blending each component + * of the two given ARGB colors. + * + * @param dst + * The destination ARGB color. + * + * @param src + * The source ARGB color. + * + * @return + * The result of applying the Porter-Duff "over" composite operator to the + * given source and destination colors. + */ +static uint32_t guac_common_surface_argb_blend(uint32_t dst, uint32_t src) { + + /* Separate destination ARGB color into its components */ + int dst_a = (dst >> 24) & 0xFF; + int dst_r = (dst >> 16) & 0xFF; + int dst_g = (dst >> 8) & 0xFF; + int dst_b = dst & 0xFF; + + /* Separate source ARGB color into its components */ + int src_a = (src >> 24) & 0xFF; + int src_r = (src >> 16) & 0xFF; + int src_g = (src >> 8) & 0xFF; + int src_b = src & 0xFF; + + /* If source is fully opaque (or destination is fully transparent), the + * blended result is the source */ + if (src_a == 0xFF || dst_a == 0x00) + return src; + + /* If source is fully transparent, the blended result is the destination */ + if (src_a == 0x00) + return dst; + + /* Otherwise, blend each ARGB component, assuming pre-multiplied alpha */ + int r = guac_common_surface_blend_component(dst_r, src_r, src_a); + int g = guac_common_surface_blend_component(dst_g, src_g, src_a); + int b = guac_common_surface_blend_component(dst_b, src_b, src_a); + int a = guac_common_surface_blend_component(dst_a, src_a, src_a); + + /* Recombine blended components */ + return (a << 24) | (r << 16) | (g << 8) | b; + +} + +/** + * Copies data from the given buffer to the surface at the given coordinates. + * The dimensions and location of the destination rectangle will be altered + * to remove as many unchanged pixels as possible. + * + * @param src_buffer The buffer to copy. + * @param src_stride The number of bytes in each row of the source buffer. + * @param sx The X coordinate of the source rectangle. + * @param sy The Y coordinate of the source rectangle. + * @param dst The destination surface. + * @param rect The destination rectangle. + * @param opaque Non-zero if the source surface is opaque (its alpha channel + * should be ignored), zero otherwise. + */ +static void __guac_common_surface_put(unsigned char* src_buffer, int src_stride, + int* sx, int* sy, + guac_common_surface* dst, guac_common_rect* rect, + int opaque) { + + unsigned char* dst_buffer = dst->buffer; + int dst_stride = dst->stride; + + int x, y; + + int min_x = rect->width; + int min_y = rect->height; + int max_x = 0; + int max_y = 0; + + int orig_x = rect->x; + int orig_y = rect->y; + + src_buffer += src_stride * (*sy) + 4 * (*sx); + dst_buffer += (dst_stride * rect->y) + (4 * rect->x); + + /* For each row */ + for (y=0; y < rect->height; y++) { + + uint32_t* src_current = (uint32_t*) src_buffer; + uint32_t* dst_current = (uint32_t*) dst_buffer; + + /* Copy row */ + for (x=0; x < rect->width; x++) { + + uint32_t color; + + /* Get source and destination color values */ + uint32_t src_color = *src_current; + uint32_t dst_color = *dst_current; + + /* Ignore alpha channel if opaque */ + if (opaque) + color = src_color | 0xFF000000; + + /* Otherwise, perform alpha blending operation */ + else + color = guac_common_surface_argb_blend(dst_color, src_color); + + /* If the destination color is changing, update rectangle bounds + * and store the new color */ + if (dst_color != color) { + if (x < min_x) min_x = x; + if (y < min_y) min_y = y; + if (x > max_x) max_x = x; + if (y > max_y) max_y = y; + *dst_current = color; + } + + /* Advance to next pixel */ + src_current++; + dst_current++; + + } + + /* Next row */ + src_buffer += src_stride; + dst_buffer += dst_stride; + + } + + /* Restrict destination rect to only updated pixels */ + if (max_x >= min_x && max_y >= min_y) { + rect->x += min_x; + rect->y += min_y; + rect->width = max_x - min_x + 1; + rect->height = max_y - min_y + 1; + } + else { + rect->width = 0; + rect->height = 0; + } + + /* Update source X/Y */ + *sx += rect->x - orig_x; + *sy += rect->y - orig_y; + +} + +/** + * Fills the given surface with color, using the given buffer as a mask. Color + * will be added to the given surface iff the corresponding pixel within the + * buffer is opaque. + * + * @param src_buffer The buffer to use as a mask. + * @param src_stride The number of bytes in each row of the source buffer. + * @param sx The X coordinate of the source rectangle. + * @param sy The Y coordinate of the source rectangle. + * @param dst The destination surface. + * @param rect The destination rectangle. + * @param red The red component of the color of the fill. + * @param green The green component of the color of the fill. + * @param blue The blue component of the color of the fill. + */ +static void __guac_common_surface_fill_mask(unsigned char* src_buffer, int src_stride, + int sx, int sy, + guac_common_surface* dst, guac_common_rect* rect, + int red, int green, int blue) { + + unsigned char* dst_buffer = dst->buffer; + int dst_stride = dst->stride; + + uint32_t color = 0xFF000000 | (red << 16) | (green << 8) | blue; + int x, y; + + src_buffer += src_stride*sy + 4*sx; + dst_buffer += (dst_stride * rect->y) + (4 * rect->x); + + /* For each row */ + for (y=0; y < rect->height; y++) { + + uint32_t* src_current = (uint32_t*) src_buffer; + uint32_t* dst_current = (uint32_t*) dst_buffer; + + /* Stencil row */ + for (x=0; x < rect->width; x++) { + + /* Fill with color if opaque */ + if (*src_current & 0xFF000000) + *dst_current = color; + + src_current++; + dst_current++; + } + + /* Next row */ + src_buffer += src_stride; + dst_buffer += dst_stride; + + } + +} + +/** + * Copies data from the given surface to the given destination surface using + * the specified transfer function. + * + * @param src_buffer The buffer to copy. + * @param src_stride The number of bytes in each row of the source buffer. + * @param sx The X coordinate of the source rectangle. + * @param sy The Y coordinate of the source rectangle. + * @param op The transfer function to use. + * @param dst The destination surface. + * @param rect The destination rectangle. + */ +static void __guac_common_surface_transfer(guac_common_surface* src, int* sx, int* sy, + guac_transfer_function op, + guac_common_surface* dst, guac_common_rect* rect) { + + unsigned char* src_buffer = src->buffer; + unsigned char* dst_buffer = dst->buffer; + + int x, y; + int src_stride, dst_stride; + int step = 1; + + int min_x = rect->width - 1; + int min_y = rect->height - 1; + int max_x = 0; + int max_y = 0; + + int orig_x = rect->x; + int orig_y = rect->y; + + /* Copy forwards only if destination is in a different surface or is before source */ + if (src != dst || rect->y < *sy || (rect->y == *sy && rect->x < *sx)) { + src_buffer += src->stride * (*sy) + 4 * (*sx); + dst_buffer += (dst->stride * rect->y) + (4 * rect->x); + src_stride = src->stride; + dst_stride = dst->stride; + step = 1; + } + + /* Otherwise, copy backwards */ + else { + src_buffer += src->stride * (*sy + rect->height - 1) + 4 * (*sx + rect->width - 1); + dst_buffer += dst->stride * (rect->y + rect->height - 1) + 4 * (rect->x + rect->width - 1); + src_stride = -src->stride; + dst_stride = -dst->stride; + step = -1; + } + + /* For each row */ + for (y=0; y < rect->height; y++) { + + uint32_t* src_current = (uint32_t*) src_buffer; + uint32_t* dst_current = (uint32_t*) dst_buffer; + + /* Transfer each pixel in row */ + for (x=0; x < rect->width; x++) { + + if (__guac_common_surface_transfer_int(op, src_current, dst_current)) { + if (x < min_x) min_x = x; + if (y < min_y) min_y = y; + if (x > max_x) max_x = x; + if (y > max_y) max_y = y; + } + + src_current += step; + dst_current += step; + } + + /* Next row */ + src_buffer += src_stride; + dst_buffer += dst_stride; + + } + + /* Translate X coordinate space of moving backwards */ + if (step < 0) { + int old_max_x = max_x; + max_x = rect->width - 1 - min_x; + min_x = rect->width - 1 - old_max_x; + } + + /* Translate Y coordinate space of moving backwards */ + if (dst_stride < 0) { + int old_max_y = max_y; + max_y = rect->height - 1 - min_y; + min_y = rect->height - 1 - old_max_y; + } + + /* Restrict destination rect to only updated pixels */ + if (max_x >= min_x && max_y >= min_y) { + rect->x += min_x; + rect->y += min_y; + rect->width = max_x - min_x + 1; + rect->height = max_y - min_y + 1; + } + else { + rect->width = 0; + rect->height = 0; + } + + /* Update source X/Y */ + *sx += rect->x - orig_x; + *sy += rect->y - orig_y; + +} + +guac_common_surface* guac_common_surface_alloc(guac_client* client, + guac_socket* socket, const guac_layer* layer, int w, int h) { + + /* Calculate heat map dimensions */ + size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(w); + size_t heat_height = GUAC_COMMON_SURFACE_HEAT_DIMENSION(h); + + /* Init surface */ + guac_common_surface* surface = guac_mem_zalloc(sizeof(guac_common_surface)); + surface->client = client; + surface->socket = socket; + surface->layer = layer; + surface->parent = GUAC_DEFAULT_LAYER; + surface->opacity = 0xFF; + surface->width = w; + surface->height = h; + + pthread_mutex_init(&surface->_lock, NULL); + + /* Create corresponding Cairo surface */ + surface->stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); + surface->buffer = guac_mem_zalloc(h, surface->stride); + + /* Create corresponding heat map */ + surface->heat_map = guac_mem_zalloc(heat_width, heat_height, + sizeof(guac_common_surface_heat_cell)); + + /* Reset clipping rect */ + guac_common_surface_reset_clip(surface); + + /* Layers must initially exist */ + if (layer->index >= 0) { + guac_protocol_send_size(socket, layer, w, h); + surface->realized = 1; + } + + /* Defer creation of buffers */ + else + surface->realized = 0; + + return surface; +} + +void guac_common_surface_free(guac_common_surface* surface) { + + /* Only dispose of surface if it exists */ + if (surface->realized) + guac_protocol_send_dispose(surface->socket, surface->layer); + + pthread_mutex_destroy(&surface->_lock); + + guac_mem_free(surface->heat_map); + guac_mem_free(surface->buffer); + guac_mem_free(surface); + +} + +void guac_common_surface_resize(guac_common_surface* surface, int w, int h) { + + pthread_mutex_lock(&surface->_lock); + + /* Ignore if resize will have no effect */ + if (w == surface->width && h == surface->height) + goto complete; + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + unsigned char* old_buffer; + int old_stride; + guac_common_rect old_rect; + + int sx = 0; + int sy = 0; + + /* Calculate heat map dimensions */ + size_t heat_width = GUAC_COMMON_SURFACE_HEAT_DIMENSION(w); + size_t heat_height = GUAC_COMMON_SURFACE_HEAT_DIMENSION(h); + + /* Copy old surface data */ + old_buffer = surface->buffer; + old_stride = surface->stride; + guac_common_rect_init(&old_rect, 0, 0, surface->width, surface->height); + + /* Re-initialize at new size */ + surface->width = w; + surface->height = h; + surface->stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); + surface->buffer = guac_mem_zalloc(h, surface->stride); + __guac_common_bound_rect(surface, &surface->clip_rect, NULL, NULL); + + /* Copy relevant old data */ + __guac_common_bound_rect(surface, &old_rect, NULL, NULL); + __guac_common_surface_put(old_buffer, old_stride, &sx, &sy, surface, &old_rect, 1); + + /* Free old data */ + guac_mem_free(old_buffer); + + /* Allocate completely new heat map (can safely discard old stats) */ + guac_mem_free(surface->heat_map); + surface->heat_map = guac_mem_zalloc(heat_width, heat_height, + sizeof(guac_common_surface_heat_cell)); + + /* Resize dirty rect to fit new surface dimensions */ + if (surface->dirty) { + __guac_common_bound_rect(surface, &surface->dirty_rect, NULL, NULL); + if (surface->dirty_rect.width <= 0 || surface->dirty_rect.height <= 0) + surface->dirty = 0; + } + + /* Update Guacamole layer */ + if (surface->realized) + guac_protocol_send_size(socket, layer, w, h); + +complete: + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_draw(guac_common_surface* surface, int x, int y, cairo_surface_t* src) { + + pthread_mutex_lock(&surface->_lock); + + unsigned char* buffer = cairo_image_surface_get_data(src); + cairo_format_t format = cairo_image_surface_get_format(src); + int stride = cairo_image_surface_get_stride(src); + int w = cairo_image_surface_get_width(src); + int h = cairo_image_surface_get_height(src); + + int sx = 0; + int sy = 0; + + guac_common_rect rect; + guac_common_rect_init(&rect, x, y, w, h); + + /* Clip operation */ + __guac_common_clip_rect(surface, &rect, &sx, &sy); + if (rect.width <= 0 || rect.height <= 0) + goto complete; + + /* Update backing surface */ + __guac_common_surface_put(buffer, stride, &sx, &sy, surface, &rect, format != CAIRO_FORMAT_ARGB32); + if (rect.width <= 0 || rect.height <= 0) + goto complete; + + /* Update the heat map for the update rectangle. */ + guac_timestamp time = guac_timestamp_current(); + __guac_common_surface_touch_rect(surface, &rect, time); + + /* Flush if not combining */ + if (!__guac_common_should_combine(surface, &rect, 0)) + __guac_common_surface_flush_deferred(surface); + + /* Always defer draws */ + __guac_common_mark_dirty(surface, &rect); + +complete: + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_paint(guac_common_surface* surface, int x, int y, + cairo_surface_t* src, int red, int green, int blue) { + + pthread_mutex_lock(&surface->_lock); + + unsigned char* buffer = cairo_image_surface_get_data(src); + int stride = cairo_image_surface_get_stride(src); + int w = cairo_image_surface_get_width(src); + int h = cairo_image_surface_get_height(src); + + int sx = 0; + int sy = 0; + + guac_common_rect rect; + guac_common_rect_init(&rect, x, y, w, h); + + /* Clip operation */ + __guac_common_clip_rect(surface, &rect, &sx, &sy); + if (rect.width <= 0 || rect.height <= 0) + goto complete; + + /* Update backing surface */ + __guac_common_surface_fill_mask(buffer, stride, sx, sy, surface, &rect, red, green, blue); + + /* Flush if not combining */ + if (!__guac_common_should_combine(surface, &rect, 0)) + __guac_common_surface_flush_deferred(surface); + + /* Always defer draws */ + __guac_common_mark_dirty(surface, &rect); + +complete: + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_copy(guac_common_surface* src, int sx, int sy, + int w, int h, guac_common_surface* dst, int dx, int dy) { + + /* Lock both surfaces */ + pthread_mutex_lock(&dst->_lock); + if (src != dst) + pthread_mutex_lock(&src->_lock); + + guac_socket* socket = dst->socket; + const guac_layer* src_layer = src->layer; + const guac_layer* dst_layer = dst->layer; + + guac_common_rect srect; + guac_common_rect_init(&srect, sx, sy, w, h); + + /* Clip operation source rect to bounds */ + __guac_common_bound_rect(src, &srect, &dx, &dy); + if (srect.width <= 0 || srect.height <= 0) + goto complete; + + guac_common_rect drect; + guac_common_rect_init(&drect, dx, dy, + srect.width, srect.height); + + /* Clip operation destination rect */ + __guac_common_clip_rect(dst, &drect, &srect.x, &srect.y); + if (drect.width <= 0 || drect.height <= 0) + goto complete; + + /* NOTE: Being the last rectangle to be adjusted, only the width/height of + * drect is now correct! */ + + /* Update backing surface first only if drect cannot intersect srect */ + if (src != dst) { + __guac_common_surface_transfer(src, &srect.x, &srect.y, + GUAC_TRANSFER_BINARY_SRC, dst, &drect); + if (drect.width <= 0 || drect.height <= 0) + goto complete; + } + + /* Defer if combining */ + if (__guac_common_should_combine(dst, &drect, 1)) + __guac_common_mark_dirty(dst, &drect); + + /* Otherwise, flush and draw immediately */ + else { + __guac_common_surface_flush(dst); + __guac_common_surface_flush(src); + guac_protocol_send_copy(socket, src_layer, srect.x, srect.y, + drect.width, drect.height, GUAC_COMP_OVER, dst_layer, + drect.x, drect.y); + dst->realized = 1; + } + + /* Update backing surface last if drect can intersect srect */ + if (src == dst) + __guac_common_surface_transfer(src, &srect.x, &srect.y, + GUAC_TRANSFER_BINARY_SRC, dst, &drect); + +complete: + + /* Unlock both surfaces */ + pthread_mutex_unlock(&dst->_lock); + if (src != dst) + pthread_mutex_unlock(&src->_lock); + +} + +void guac_common_surface_transfer(guac_common_surface* src, int sx, int sy, int w, int h, + guac_transfer_function op, guac_common_surface* dst, int dx, int dy) { + + /* Lock both surfaces */ + pthread_mutex_lock(&dst->_lock); + if (src != dst) + pthread_mutex_lock(&src->_lock); + + guac_socket* socket = dst->socket; + const guac_layer* src_layer = src->layer; + const guac_layer* dst_layer = dst->layer; + + guac_common_rect srect; + guac_common_rect_init(&srect, sx, sy, w, h); + + /* Clip operation source rect to bounds */ + __guac_common_bound_rect(src, &srect, &dx, &dy); + if (srect.width <= 0 || srect.height <= 0) + goto complete; + + guac_common_rect drect; + guac_common_rect_init(&drect, dx, dy, + srect.width, srect.height); + + /* Clip operation destination rect */ + __guac_common_clip_rect(dst, &drect, &srect.x, &srect.y); + if (drect.width <= 0 || drect.height <= 0) + goto complete; + + /* NOTE: Being the last rectangle to be adjusted, only the width/height of + * drect is now correct! */ + + /* Update backing surface first only if drect cannot intersect srect */ + if (src != dst) { + __guac_common_surface_transfer(src, &srect.x, &srect.y, op, dst, &drect); + if (drect.width <= 0 || drect.height <= 0) + goto complete; + } + + /* Defer if combining */ + if (__guac_common_should_combine(dst, &drect, 1)) + __guac_common_mark_dirty(dst, &drect); + + /* Otherwise, flush and draw immediately */ + else { + __guac_common_surface_flush(dst); + __guac_common_surface_flush(src); + guac_protocol_send_transfer(socket, src_layer, srect.x, srect.y, + drect.width, drect.height, op, dst_layer, drect.x, drect.y); + dst->realized = 1; + } + + /* Update backing surface last if drect can intersect srect */ + if (src == dst) + __guac_common_surface_transfer(src, &srect.x, &srect.y, op, dst, &drect); + +complete: + + /* Unlock both surfaces */ + pthread_mutex_unlock(&dst->_lock); + if (src != dst) + pthread_mutex_unlock(&src->_lock); + +} + +void guac_common_surface_set(guac_common_surface* surface, + int x, int y, int w, int h, int red, int green, int blue, int alpha) { + + pthread_mutex_lock(&surface->_lock); + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + guac_common_rect rect; + guac_common_rect_init(&rect, x, y, w, h); + + /* Clip operation */ + __guac_common_clip_rect(surface, &rect, NULL, NULL); + if (rect.width <= 0 || rect.height <= 0) + goto complete; + + /* Update backing surface */ + __guac_common_surface_set(surface, &rect, red, green, blue, alpha); + if (rect.width <= 0 || rect.height <= 0) + goto complete; + + /* Handle as normal draw if non-opaque */ + if (alpha != 0xFF) { + + /* Flush if not combining */ + if (!__guac_common_should_combine(surface, &rect, 0)) + __guac_common_surface_flush_deferred(surface); + + /* Always defer draws */ + __guac_common_mark_dirty(surface, &rect); + + } + + /* Defer if combining */ + else if (__guac_common_should_combine(surface, &rect, 1)) + __guac_common_mark_dirty(surface, &rect); + + /* Otherwise, flush and draw immediately */ + else { + __guac_common_surface_flush(surface); + guac_protocol_send_rect(socket, layer, rect.x, rect.y, rect.width, rect.height); + guac_protocol_send_cfill(socket, GUAC_COMP_OVER, layer, red, green, blue, alpha); + surface->realized = 1; + } + +complete: + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_clip(guac_common_surface* surface, int x, int y, int w, int h) { + + pthread_mutex_lock(&surface->_lock); + + guac_common_rect clip; + + /* Init clipping rectangle if clipping not already applied */ + if (!surface->clipped) { + guac_common_rect_init(&surface->clip_rect, 0, 0, surface->width, surface->height); + surface->clipped = 1; + } + + guac_common_rect_init(&clip, x, y, w, h); + guac_common_rect_constrain(&surface->clip_rect, &clip); + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_reset_clip(guac_common_surface* surface) { + pthread_mutex_lock(&surface->_lock); + surface->clipped = 0; + pthread_mutex_unlock(&surface->_lock); +} + +/** + * Flushes the bitmap update currently described by the dirty rectangle within + * the given surface directly via an "img" instruction as PNG data. The + * resulting instructions will be sent over the socket associated with the + * given surface. + * + * @param surface + * The surface to flush. + * + * @param opaque + * Whether the rectangle being flushed contains only fully-opaque pixels. + */ +static void __guac_common_surface_flush_to_png(guac_common_surface* surface, + int opaque) { + + if (surface->dirty) { + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + /* Get Cairo surface for specified rect */ + unsigned char* buffer = surface->buffer + + surface->dirty_rect.y * surface->stride + + surface->dirty_rect.x * 4; + + cairo_surface_t* rect; + + /* Use RGB24 if the image is fully opaque */ + if (opaque) + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, surface->dirty_rect.width, + surface->dirty_rect.height, surface->stride); + + /* Otherwise ARGB32 is needed */ + else { + + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_ARGB32, surface->dirty_rect.width, + surface->dirty_rect.height, surface->stride); + + /* Clear destination rect first */ + guac_protocol_send_rect(socket, layer, + surface->dirty_rect.x, surface->dirty_rect.y, + surface->dirty_rect.width, surface->dirty_rect.height); + guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, + 0x00, 0x00, 0x00, 0xFF); + + } + + /* Send PNG for rect */ + guac_client_stream_png(surface->client, socket, GUAC_COMP_OVER, + layer, surface->dirty_rect.x, surface->dirty_rect.y, rect); + + cairo_surface_destroy(rect); + surface->realized = 1; + + /* Surface is no longer dirty */ + surface->dirty = 0; + + } + +} + +/** + * 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_common_surface_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; + +} + +/** + * Flushes the bitmap update currently described by the dirty rectangle within + * the given surface directly via an "img" instruction as JPEG data. The + * resulting instructions will be sent over the socket associated with the + * given surface. + * + * @param surface + * The surface to flush. + */ +static void __guac_common_surface_flush_to_jpeg(guac_common_surface* surface) { + + if (surface->dirty) { + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + guac_common_rect max; + guac_common_rect_init(&max, 0, 0, surface->width, surface->height); + + /* Expand the dirty rect size to fit in a grid with cells equal to the + * minimum JPEG block size */ + guac_common_rect_expand_to_grid(GUAC_SURFACE_JPEG_BLOCK_SIZE, + &surface->dirty_rect, &max); + + /* Get Cairo surface for specified rect */ + unsigned char* buffer = surface->buffer + + surface->dirty_rect.y * surface->stride + + surface->dirty_rect.x * 4; + + cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, surface->dirty_rect.width, + surface->dirty_rect.height, surface->stride); + + /* Send JPEG for rect */ + guac_client_stream_jpeg(surface->client, socket, GUAC_COMP_OVER, layer, + surface->dirty_rect.x, surface->dirty_rect.y, rect, + guac_common_surface_suggest_quality(surface->client)); + + cairo_surface_destroy(rect); + surface->realized = 1; + + /* Surface is no longer dirty */ + surface->dirty = 0; + + } + +} + +/** + * Flushes the bitmap update currently described by the dirty rectangle within + * the given surface directly via an "img" instruction as WebP data. The + * resulting instructions will be sent over the socket associated with the + * given surface. + * + * @param surface + * The surface to flush. + * + * @param opaque + * Whether the rectangle being flushed contains only fully-opaque pixels. + */ +static void __guac_common_surface_flush_to_webp(guac_common_surface* surface, + int opaque) { + + if (surface->dirty) { + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + guac_common_rect max; + guac_common_rect_init(&max, 0, 0, surface->width, surface->height); + + /* Expand the dirty rect size to fit in a grid with cells equal to the + * minimum WebP block size */ + guac_common_rect_expand_to_grid(GUAC_SURFACE_WEBP_BLOCK_SIZE, + &surface->dirty_rect, &max); + + /* Get Cairo surface for specified rect */ + unsigned char* buffer = surface->buffer + + surface->dirty_rect.y * surface->stride + + surface->dirty_rect.x * 4; + + cairo_surface_t* rect; + + /* Use RGB24 if the image is fully opaque */ + if (opaque) + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, surface->dirty_rect.width, + surface->dirty_rect.height, surface->stride); + + /* Otherwise ARGB32 is needed */ + else + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_ARGB32, surface->dirty_rect.width, + surface->dirty_rect.height, surface->stride); + + /* Send WebP for rect */ + guac_client_stream_webp(surface->client, socket, GUAC_COMP_OVER, layer, + surface->dirty_rect.x, surface->dirty_rect.y, rect, + guac_common_surface_suggest_quality(surface->client), + surface->lossless ? 1 : 0); + + cairo_surface_destroy(rect); + surface->realized = 1; + + /* Surface is no longer dirty */ + surface->dirty = 0; + + } + +} + +/** + * Comparator for instances of guac_common_surface_bitmap_rect, the elements + * which make up a surface's bitmap buffer. + * + * @see qsort + */ +static int __guac_common_surface_bitmap_rect_compare(const void* a, const void* b) { + + guac_common_surface_bitmap_rect* ra = (guac_common_surface_bitmap_rect*) a; + guac_common_surface_bitmap_rect* rb = (guac_common_surface_bitmap_rect*) b; + + /* Order roughly top to bottom, left to right */ + if (ra->rect.y != rb->rect.y) return ra->rect.y - rb->rect.y; + if (ra->rect.x != rb->rect.x) return ra->rect.x - rb->rect.x; + + /* Wider updates should come first (more likely to intersect later) */ + if (ra->rect.width != rb->rect.width) return rb->rect.width - ra->rect.width; + + /* Shorter updates should come first (less likely to increase cost) */ + return ra->rect.height - rb->rect.height; + +} + +/** + * Flushes only the properties of the given surface, such as layer location or + * opacity. Image state is not flushed. If the surface represents a buffer or + * the default layer, this function has no effect. + * + * @param surface + * The surface to flush. + */ +static void __guac_common_surface_flush_properties( + guac_common_surface* surface) { + + guac_socket* socket = surface->socket; + + /* Only applicable to non-default visible layers */ + if (surface->layer->index <= 0) + return; + + /* Flush opacity */ + if (surface->opacity_dirty) { + guac_protocol_send_shade(socket, surface->layer, surface->opacity); + surface->opacity_dirty = 0; + } + + /* Flush location and hierarchy */ + if (surface->location_dirty) { + guac_protocol_send_move(socket, surface->layer, + surface->parent, surface->x, surface->y, surface->z); + surface->location_dirty = 0; + } + +} + +static void __guac_common_surface_flush(guac_common_surface* surface) { + + /* Flush final dirty rectangle to queue. */ + __guac_common_surface_flush_to_queue(surface); + + guac_common_surface_bitmap_rect* current = surface->bitmap_queue; + int i, j; + int original_queue_length; + int flushed = 0; + + original_queue_length = surface->bitmap_queue_length; + + /* Sort updates to make combination less costly */ + qsort(surface->bitmap_queue, surface->bitmap_queue_length, sizeof(guac_common_surface_bitmap_rect), + __guac_common_surface_bitmap_rect_compare); + + /* Flush all rects in queue */ + for (i=0; i < surface->bitmap_queue_length; i++) { + + /* Get next unflushed candidate */ + guac_common_surface_bitmap_rect* candidate = current; + if (!candidate->flushed) { + + int combined = 0; + + /* Build up rect as much as possible */ + for (j=i; j < surface->bitmap_queue_length; j++) { + + if (!candidate->flushed) { + + /* Clip candidate within current bounds */ + __guac_common_bound_rect(surface, &candidate->rect, NULL, NULL); + if (candidate->rect.width <= 0 || candidate->rect.height <= 0) + candidate->flushed = 1; + + /* Combine if reasonable */ + else if (__guac_common_should_combine(surface, &candidate->rect, 0) || !surface->dirty) { + __guac_common_mark_dirty(surface, &candidate->rect); + candidate->flushed = 1; + combined++; + } + + } + + candidate++; + + } + + /* Re-add to queue if there's room and this update was modified or we expect others might be */ + if ((combined > 1 || i < original_queue_length) + && surface->bitmap_queue_length < GUAC_COMMON_SURFACE_QUEUE_SIZE) + __guac_common_surface_flush_to_queue(surface); + + /* Flush as bitmap otherwise */ + else if (surface->dirty) { + + flushed++; + + int opaque = __guac_common_surface_is_opaque(surface, + &surface->dirty_rect); + + /* Prefer WebP when reasonable */ + if (__guac_common_surface_should_use_webp(surface, + &surface->dirty_rect)) + __guac_common_surface_flush_to_webp(surface, opaque); + + /* If not WebP, JPEG is the next best (lossy) choice */ + else if (opaque && __guac_common_surface_should_use_jpeg( + surface, &surface->dirty_rect)) + __guac_common_surface_flush_to_jpeg(surface); + + /* Use PNG if no lossy formats are appropriate */ + else + __guac_common_surface_flush_to_png(surface, opaque); + + } + + } + + current++; + + } + + /* Flush complete */ + surface->bitmap_queue_length = 0; + +} + +void guac_common_surface_flush(guac_common_surface* surface) { + + pthread_mutex_lock(&surface->_lock); + + /* Flush any applicable layer properties */ + __guac_common_surface_flush_properties(surface); + + /* Flush surface contents */ + __guac_common_surface_flush(surface); + + pthread_mutex_unlock(&surface->_lock); + +} + +void guac_common_surface_dup(guac_common_surface* surface, + guac_client* client, guac_socket* socket) { + + pthread_mutex_lock(&surface->_lock); + + /* Do nothing if not realized */ + if (!surface->realized) + goto complete; + + /* Synchronize layer-specific properties if applicable */ + if (surface->layer->index > 0) { + + /* Synchronize opacity */ + guac_protocol_send_shade(socket, surface->layer, surface->opacity); + + /* Synchronize location and hierarchy */ + guac_protocol_send_move(socket, surface->layer, + surface->parent, surface->x, surface->y, surface->z); + + } + + /* Synchronize multi-touch support level */ + else if (surface->layer->index == 0) + guac_protocol_send_set_int(socket, surface->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, + surface->touches); + + /* Sync size to new socket */ + guac_protocol_send_size(socket, surface->layer, + surface->width, surface->height); + + /* Send contents of layer, if non-empty */ + if (surface->width > 0 && surface->height > 0) { + + /* Get entire surface */ + cairo_surface_t* rect = cairo_image_surface_create_for_data( + surface->buffer, CAIRO_FORMAT_ARGB32, + surface->width, surface->height, surface->stride); + + /* Send PNG for rect */ + guac_client_stream_png(client, socket, GUAC_COMP_OVER, surface->layer, + 0, 0, rect); + cairo_surface_destroy(rect); + + } + +complete: + pthread_mutex_unlock(&surface->_lock); + +} diff --git a/src/common/tests/Makefile.am b/src/common/tests/Makefile.am index b84dc827f..27ac75cd1 100644 --- a/src/common/tests/Makefile.am +++ b/src/common/tests/Makefile.am @@ -39,6 +39,12 @@ noinst_HEADERS = \ test_common_SOURCES = \ iconv/convert.c \ iconv/convert-test-data.c \ + rect/clip_and_split.c \ + rect/constrain.c \ + rect/expand_to_grid.c \ + rect/extend.c \ + rect/init.c \ + rect/intersects.c \ string/count_occurrences.c \ string/split.c diff --git a/src/common/tests/rect/clip_and_split.c b/src/common/tests/rect/clip_and_split.c new file mode 100644 index 000000000..e286bce9f --- /dev/null +++ b/src/common/tests/rect/clip_and_split.c @@ -0,0 +1,156 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_clip_and_split() divides a + * rectangle into subrectangles after removing a "hole" rectangle. + */ +void test_rect__clip_and_split() { + + int res; + + guac_common_rect cut; + guac_common_rect min; + guac_common_rect rect; + + guac_common_rect_init(&min, 10, 10, 10, 10); + + /* Clip top */ + guac_common_rect_init(&rect, 10, 5, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(5, cut.y); + CU_ASSERT_EQUAL(10, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(5, rect.height); + + /* Clip bottom */ + guac_common_rect_init(&rect, 10, 15, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(20, cut.y); + CU_ASSERT_EQUAL(10, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(15, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(5, rect.height); + + /* Clip left */ + guac_common_rect_init(&rect, 5, 10, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(5, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Clip right */ + guac_common_rect_init(&rect, 15, 10, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(20, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(15, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(5, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* + * Test a rectangle which completely covers the hole. + * Clip and split until done. + */ + guac_common_rect_init(&rect, 5, 5, 20, 20); + + /* Clip top */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(5, cut.y); + CU_ASSERT_EQUAL(20, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(5, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(20, rect.width); + CU_ASSERT_EQUAL(15, rect.height); + + /* Clip left */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(15, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(15, rect.width); + CU_ASSERT_EQUAL(15, rect.height); + + /* Clip bottom */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(20, cut.y); + CU_ASSERT_EQUAL(15, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(15, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Clip right */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(20, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Make sure nothing is left to do */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(0, res); + +} + diff --git a/src/common/tests/rect/constrain.c b/src/common/tests/rect/constrain.c new file mode 100644 index 000000000..793aac224 --- /dev/null +++ b/src/common/tests/rect/constrain.c @@ -0,0 +1,43 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_constrain() restricts a given + * rectangle to arbitrary bounds. + */ +void test_rect__constrain() { + + guac_common_rect max; + guac_common_rect rect; + + guac_common_rect_init(&rect, -10, -10, 110, 110); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_constrain(&rect, &max); + + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(100, rect.width); + CU_ASSERT_EQUAL(100, rect.height); + +} + diff --git a/src/common/tests/rect/expand_to_grid.c b/src/common/tests/rect/expand_to_grid.c new file mode 100644 index 000000000..beef87d89 --- /dev/null +++ b/src/common/tests/rect/expand_to_grid.c @@ -0,0 +1,71 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies guac_common_rect_expand_to_grid() properly shifts and + * resizes rectangles to fit an NxN grid. + */ +void test_rect__expand_to_grid() { + + int cell_size = 16; + + guac_common_rect max; + guac_common_rect rect; + + /* Simple adjustment */ + guac_common_rect_init(&rect, 0, 0, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + /* Adjustment with moving of rect */ + guac_common_rect_init(&rect, 75, 75, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(max.width - 32, rect.x); + CU_ASSERT_EQUAL(max.height - 32, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + guac_common_rect_init(&rect, -5, -5, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + /* Adjustment with moving and clamping of rect */ + guac_common_rect_init(&rect, 0, 0, 25, 15); + guac_common_rect_init(&max, 0, 5, 32, 15); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(max.x, rect.x); + CU_ASSERT_EQUAL(max.y, rect.y); + CU_ASSERT_EQUAL(max.width, rect.width); + CU_ASSERT_EQUAL(max.height, rect.height); + +} + diff --git a/src/common/tests/rect/extend.c b/src/common/tests/rect/extend.c new file mode 100644 index 000000000..dc2a0e308 --- /dev/null +++ b/src/common/tests/rect/extend.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 "common/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_extend() expands the given + * rectangle as necessary to contain at least the given bounds. + */ +void test_rect__extend() { + + guac_common_rect max; + guac_common_rect rect; + + guac_common_rect_init(&rect, 10, 10, 90, 90); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_extend(&rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(100, rect.width); + CU_ASSERT_EQUAL(100, rect.height); + +} + diff --git a/src/common/tests/rect/init.c b/src/common/tests/rect/init.c new file mode 100644 index 000000000..288cd751b --- /dev/null +++ b/src/common/tests/rect/init.c @@ -0,0 +1,39 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies rectangle initialization via guac_common_rect_init(). + */ +void test_rect__init() { + + guac_common_rect max; + + guac_common_rect_init(&max, 0, 0, 100, 100); + + CU_ASSERT_EQUAL(0, max.x); + CU_ASSERT_EQUAL(0, max.y); + CU_ASSERT_EQUAL(100, max.width); + CU_ASSERT_EQUAL(100, max.height); + +} + diff --git a/src/common/tests/rect/intersects.c b/src/common/tests/rect/intersects.c new file mode 100644 index 000000000..c48026844 --- /dev/null +++ b/src/common/tests/rect/intersects.c @@ -0,0 +1,91 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies intersection testing via guac_common_rect_intersects(). + */ +void test_rect__intersects() { + + int res; + + guac_common_rect min; + guac_common_rect rect; + + guac_common_rect_init(&min, 10, 10, 10, 10); + + /* Rectangle intersection - empty + * rectangle is outside */ + guac_common_rect_init(&rect, 25, 25, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(0, res); + + /* Rectangle intersection - complete + * rectangle is completely inside */ + guac_common_rect_init(&rect, 11, 11, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects UL */ + guac_common_rect_init(&rect, 8, 8, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - partial + * rectangle intersects LR */ + guac_common_rect_init(&rect, 18, 18, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - complete + * rect intersects along UL but inside */ + guac_common_rect_init(&rect, 10, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects along L but outside */ + guac_common_rect_init(&rect, 5, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - complete + * rectangle intersects along LR but rest is inside */ + guac_common_rect_init(&rect, 15, 15, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects along R but rest is outside */ + guac_common_rect_init(&rect, 20, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - partial + * rectangle encloses min; which is a partial intersection */ + guac_common_rect_init(&rect, 5, 5, 20, 20); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + +} + From 4145f5871c17c006863206b0f791088d3e25d922 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Sep 2024 15:42:28 -0700 Subject: [PATCH 39/53] GUACAMOLE-377: Revert migration of terminal emulator to guac_display. --- src/terminal/buffer.c | 22 + src/terminal/display.c | 963 +++++++++++++++++++------- src/terminal/scrollbar.c | 186 ++--- src/terminal/select.c | 97 ++- src/terminal/terminal.c | 480 +++++++++++-- src/terminal/terminal/buffer.h | 18 + src/terminal/terminal/display.h | 297 ++++---- src/terminal/terminal/scrollbar.h | 45 +- src/terminal/terminal/select.h | 12 + src/terminal/terminal/terminal-priv.h | 48 +- src/terminal/terminal/terminal.h | 4 +- src/terminal/terminal/types.h | 5 + 12 files changed, 1606 insertions(+), 571 deletions(-) diff --git a/src/terminal/buffer.c b/src/terminal/buffer.c index ead57e613..e558d0345 100644 --- a/src/terminal/buffer.c +++ b/src/terminal/buffer.c @@ -519,6 +519,28 @@ void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, } +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 diff --git a/src/terminal/display.c b/src/terminal/display.c index 5eaa4ce81..402b849dc 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -17,8 +17,7 @@ * under the License. */ -#include "config.h" -#include "guacamole/rect.h" +#include "common/surface.h" #include "terminal/common.h" #include "terminal/display.h" #include "terminal/palette.h" @@ -27,134 +26,43 @@ #include "terminal/types.h" #include -#include #include #include #include #include #include +#include #include -#include #include #include #include #include -/** - * The palette index of the color to use when highlighting selected text. - */ -#define GUAC_TERMINAL_HIGHLIGHT_COLOR 4 - -/** - * Calculates the approximate luminance of the given color, where 0 represents - * no luminance and 255 represents full luminance. - * - * @param color - * The color to calculate the luminance of. - * - * @return - * The approximate luminance of the given color, on a scale of 0 through - * 255 inclusive. - */ -static int guac_terminal_color_luminance(const guac_terminal_color* color) { - - /* - * Y = 0.2126 R + 0.7152 G + 0.0722 B - * - * Here we multiply all coefficients by 16 to approximate luminance without - * having to resort to floating point, rounding to the nearest integer that - * minimizes error but still totals 16 when added to the other - * coefficients. - */ - - return (3 * color->red + 12 * color->green + color->blue) / 16; - -} - -/** - * Given the foreground and background colors of a character, updates those - * colors to represent the fact that the character has been highlighted - * (selected by the user). - * - * @param display - * The terminal display containing the character. - * - * @param glyph_foreground - * The foreground color of the character. The contents of this color may be - * modified to represent the effect of the highlight. - * - * @param glyph_background - * The background color of the character. The contents of this color may be - * modified to represent the effect of the highlight. - */ -static void guac_terminal_display_apply_highlight(guac_terminal_display* display, - guac_terminal_color* glyph_foreground, guac_terminal_color* glyph_background) { - - guac_terminal_color highlight; - guac_terminal_display_lookup_color(display, GUAC_TERMINAL_HIGHLIGHT_COLOR, &highlight); - - highlight.red = (highlight.red + glyph_background->red) / 2; - highlight.green = (highlight.green + glyph_background->green) / 2; - highlight.blue = (highlight.blue + glyph_background->blue) / 2; +/* Maps any codepoint onto a number between 0 and 511 inclusive */ +int __guac_terminal_hash_codepoint(int codepoint) { - int foreground_lum = guac_terminal_color_luminance(glyph_foreground); - int background_lum = guac_terminal_color_luminance(glyph_background); - int highlight_lum = guac_terminal_color_luminance(&highlight); + /* If within one byte, just return codepoint */ + if (codepoint <= 0xFF) + return codepoint; - /* Replace background color for highlight color only if it's closer in - * perceived luminance to the backgrund color than it is to the - * foreground color (to preserve roughly the same degree of contrast) */ - if (abs(foreground_lum - highlight_lum) >= abs(background_lum - highlight_lum)) { - *glyph_background = highlight; - } - - /* If the highlight color can't be used while preserving contrast, - * simply inverting the colors will do the job */ - else { - guac_terminal_color temp = *glyph_background; - *glyph_background = *glyph_foreground; - *glyph_foreground = temp; - } + /* Otherwise, map to next 256 values */ + return (codepoint & 0xFF) + 0x100; } /** - * Given current attributes of a character, assigns foreground and background - * colors to represent that character state. - * - * @param display - * The terminal display containing the character. - * - * @param attributes - * All attributes associated with the character (bold, foreground color, - * background color, etc.). - * - * @param is_cursor - * Whether the terminal cursor is currently on top of the character. - * - * @param is_selected - * Whether the user currently has this character selected. - * - * @param glyph_foreground - * A pointer to the guac_terminal_color that should receive the foreground - * color of the character. - * - * @param glyph_background - * A pointer to the guac_terminal_color that should receive the background - * color of the character. + * Sets the attributes of the display such that future glyphs will render as + * expected. */ -static void guac_terminal_display_apply_render_attributes(guac_terminal_display* display, - guac_terminal_attributes* attributes, bool is_cursor, bool is_selected, - guac_terminal_color* glyph_foreground, - guac_terminal_color* glyph_background) { +int __guac_terminal_set_colors(guac_terminal_display* display, + guac_terminal_attributes* attributes) { const guac_terminal_color* background; const guac_terminal_color* foreground; - /* Swap foreground and background color to represent reverse video and the - * cursor (this means that reverse and is_cursor cancel each other out) */ - if (is_cursor ? !attributes->reverse : attributes->reverse) { + /* Handle reverse video */ + if (attributes->reverse != attributes->cursor) { background = &attributes->foreground; foreground = &attributes->background; } @@ -163,7 +71,7 @@ static void guac_terminal_display_apply_render_attributes(guac_terminal_display* background = &attributes->background; } - /* Represent bold with the corresponding intense (brighter) color */ + /* Handle bold */ if (attributes->bold && !attributes->half_bright && foreground->palette_index >= GUAC_TERMINAL_FIRST_DARK && foreground->palette_index <= GUAC_TERMINAL_LAST_DARK) { @@ -171,115 +79,89 @@ static void guac_terminal_display_apply_render_attributes(guac_terminal_display* + GUAC_TERMINAL_INTENSE_OFFSET]; } - *glyph_foreground = *foreground; + display->glyph_foreground = *foreground; guac_terminal_display_lookup_color(display, - foreground->palette_index, glyph_foreground); + foreground->palette_index, &display->glyph_foreground); - *glyph_background = *background; + display->glyph_background = *background; guac_terminal_display_lookup_color(display, - background->palette_index, glyph_background); + background->palette_index, &display->glyph_background); /* Modify color if half-bright (low intensity) */ if (attributes->half_bright && !attributes->bold) { - glyph_foreground->red /= 2; - glyph_foreground->green /= 2; - glyph_foreground->blue /= 2; + display->glyph_foreground.red /= 2; + display->glyph_foreground.green /= 2; + display->glyph_foreground.blue /= 2; } - /* Apply highlight if selected (NOTE: We re-swap foreground/background - * again here if the cursor is selected, as the sudden appearance of - * foreground color for an otherwise invisible character is surprising - * behavior) */ - if (is_selected) { - if (is_cursor) - guac_terminal_display_apply_highlight(display, glyph_background, glyph_foreground); - else - guac_terminal_display_apply_highlight(display, glyph_foreground, glyph_background); - } + return 0; } /** - * Renders a single character at the given row and column. The character is - * rendered immediately to the underlying guac_display and will be sent to - * connected users when the next guac_display frame is completed. - * - * @param display - * The teriminal display receiving the character. - * - * @param row - * The row coordinate of the character, where 0 is the top-most row. While - * negative values generally represent rows of the scrollback buffer, - * supplying negative values here would result in rendering outside the - * visible display area and would be nonsensical. - * - * @param col - * The column coordinate of the character, where 0 is the left-most column. - * - * @param c - * The character to render. - * - * @param is_cursor - * Whether the terminal cursor is currently on top of the character. - * - * @param is_selected - * Whether the user currently has this character selected. + * Sends the given character to the terminal at the given row and column, + * rendering the character immediately. This bypasses the guac_terminal_display + * mechanism and is intended for flushing of updates only. */ -static void guac_terminal_display_render_glyph(guac_terminal_display* display, int row, int col, - guac_terminal_char* c, bool is_cursor, bool is_selected) { +int __guac_terminal_set(guac_terminal_display* display, int row, int col, int codepoint) { + + int width; + + int bytes; + char utf8[4]; + + /* Use foreground color */ + const guac_terminal_color* color = &display->glyph_foreground; - /* Use space if no glyph */ - int codepoint = c->value; - if (!guac_terminal_has_glyph(codepoint)) - codepoint = ' '; + /* Use background color */ + const guac_terminal_color* background = &display->glyph_background; + + cairo_surface_t* surface; + cairo_t* cairo; + int surface_width, surface_height; + + PangoLayout* layout; + int layout_width, layout_height; + int ideal_layout_width, ideal_layout_height; /* Calculate width in columns */ - int width = wcwidth(codepoint); + width = wcwidth(codepoint); if (width < 0) width = 1; /* Do nothing if glyph is empty */ if (width == 0) - return; + return 0; /* Convert to UTF-8 */ - char utf8[4]; - int bytes = guac_terminal_encode_utf8(codepoint, utf8); - - int glyph_x = display->char_width * col; - int glyph_y = display->char_height * row; - int glyph_width = width * display->char_width; - int glyph_height = display->char_height; + bytes = guac_terminal_encode_utf8(codepoint, utf8); - int ideal_layout_width = glyph_width * PANGO_SCALE; - int ideal_layout_height = glyph_height * PANGO_SCALE; + surface_width = width * display->char_width; + surface_height = display->char_height; - guac_display_layer_cairo_context* context = guac_display_layer_open_cairo(display->display_layer); - cairo_t* cairo = context->cairo; + ideal_layout_width = surface_width * PANGO_SCALE; + ideal_layout_height = surface_height * PANGO_SCALE; - cairo_identity_matrix(cairo); - cairo_translate(cairo, glyph_x, glyph_y); - - guac_terminal_color foreground, background; - guac_terminal_display_apply_render_attributes(display, &c->attributes, - is_cursor, is_selected, &foreground, &background); + /* Prepare surface */ + surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, + surface_width, surface_height); + cairo = cairo_create(surface); /* Fill background */ cairo_set_source_rgb(cairo, - background.red / 255.0, - background.green / 255.0, - background.blue / 255.0); + background->red / 255.0, + background->green / 255.0, + background->blue / 255.0); - cairo_rectangle(cairo, 0, 0, glyph_width, glyph_height); + cairo_rectangle(cairo, 0, 0, surface_width, surface_height); cairo_fill(cairo); /* Get layout */ - PangoLayout* layout = pango_cairo_create_layout(cairo); + layout = pango_cairo_create_layout(cairo); pango_layout_set_font_description(layout, display->font_desc); pango_layout_set_text(layout, utf8, bytes); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); - int layout_width, layout_height; pango_layout_get_size(layout, &layout_width, &layout_height); /* If layout bigger than available space, scale it back */ @@ -299,21 +181,25 @@ static void guac_terminal_display_render_glyph(guac_terminal_display* display, i /* Draw */ cairo_set_source_rgb(cairo, - foreground.red / 255.0, - foreground.green / 255.0, - foreground.blue / 255.0); + color->red / 255.0, + color->green / 255.0, + color->blue / 255.0); cairo_move_to(cairo, 0.0, 0.0); pango_cairo_show_layout(cairo, layout); + /* Draw */ + guac_common_surface_draw(display->display_surface, + display->char_width * col, + display->char_height * row, + surface); + /* Free all */ g_object_unref(layout); + cairo_destroy(cairo); + cairo_surface_destroy(surface); - guac_rect char_rect; - guac_rect_init(&char_rect, glyph_x, glyph_y, glyph_width, glyph_height); - guac_rect_extend(&context->dirty, &char_rect); - - guac_display_layer_close_cairo(display->display_layer, context); + return 0; } @@ -331,7 +217,6 @@ static int get_margin_by_dpi(int dpi) { } guac_terminal_display* guac_terminal_display_alloc(guac_client* client, - guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]) { @@ -346,29 +231,36 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, display->char_height = 0; /* Create default surface */ - display->graphical_display = graphical_display; - display->display_layer = guac_display_alloc_layer(display->graphical_display, 1); - - /* Use blank (invisible) cursor by default */ - display->current_cursor = display->last_requested_cursor = GUAC_TERMINAL_CURSOR_BLANK; - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); + display->display_layer = guac_client_alloc_layer(client); + display->select_layer = guac_client_alloc_layer(client); + display->display_surface = guac_common_surface_alloc(client, + client->socket, display->display_layer, 0, 0); /* Never use lossy compression for terminal contents */ - guac_display_layer_set_lossless(display->display_layer, 1); + guac_common_surface_set_lossless(display->display_surface, 1); + + /* Select layer is a child of the display layer */ + guac_protocol_send_move(client->socket, display->select_layer, + display->display_layer, 0, 0, 0); /* Calculate margin size by DPI */ display->margin = get_margin_by_dpi(dpi); /* Offset the Default Layer to make margins even on all sides */ - guac_display_layer_move(display->display_layer, display->margin, display->margin); + guac_protocol_send_move(client->socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); - display->default_foreground = *foreground; - display->default_background = *background; + display->default_foreground = display->glyph_foreground = *foreground; + display->default_background = display->glyph_background = *background; display->default_palette = palette; /* Initially empty */ display->width = 0; display->height = 0; + display->operations = NULL; + + /* Initially nothing selected */ + display->text_selected = false; /* Attempt to load font */ if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) { @@ -384,15 +276,15 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, void guac_terminal_display_free(guac_terminal_display* display) { - /* Free text rendering surface */ - guac_display_free_layer(display->display_layer); - /* Free font description */ pango_font_description_free(display->font_desc); /* Free default palette. */ guac_mem_free(display->default_palette); + /* Free operations buffers */ + guac_mem_free(display->operations); + /* Free display */ guac_mem_free(display); @@ -454,105 +346,660 @@ 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) { + + /* Ignore operations outside display bounds */ + if (row < 0 || row >= display->height) + return; + + /* 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); + } + + /* Determine source and destination locations */ + + 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 (int column = start_column; column <= end_column; column++) { + + /* If no operation here, set as copy */ + if (dst->type == GUAC_CHAR_NOP) { + dst->type = GUAC_CHAR_COPY; + dst->row = row; + dst->column = column; + } + + /* Next column */ + dst++; + + } + +} + +void guac_terminal_display_copy_rows(guac_terminal_display* display, + int start_row, int end_row, int 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_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); + } + + /* 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); + + 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); + + 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 (int row = start_row; row <= end_row; row++) { + + 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) { + current->type = GUAC_CHAR_COPY; + current->row = row; + current->column = col; + } + + /* Next column */ + current++; + + } + + /* Next row */ + dst += display->width; + + } + +} + +void guac_terminal_display_set_columns(guac_terminal_display* display, int row, + int start_column, int end_column, guac_terminal_char* character) { + + /* Do nothing if glyph is empty */ + if (character->width == 0) + return; + + /* 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); + + 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 (int col = start_column; col <= end_column; col += character->width) { + + /* Set operation */ + current->type = GUAC_CHAR_SET; + current->character = *character; + + /* Next character */ + current += character->width; + + } + +} + void guac_terminal_display_resize(guac_terminal_display* display, int width, int height) { /* Resize display only if dimensions have changed */ if (width == display->width && height == display->height) return; + 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 = { + .value = 0, + .attributes = { + .foreground = display->default_background, + .background = display->default_background + }, + .width = 1 + }; + + /* Free old operations buffer */ + if (display->operations != NULL) + guac_mem_free(display->operations); + + /* Alloc operations */ + display->operations = guac_mem_alloc(width, height, + sizeof(guac_terminal_operation)); + + /* Init each operation buffer row */ + guac_terminal_operation* current = display->operations; + for (int y = 0; y < height; y++) { + + /* Init entire row to NOP */ + for (int x = 0; x < width; x++) { + + /* If on old part of screen, do not clear */ + if (x < display->width && y < display->height) + current->type = GUAC_CHAR_NOP; + + /* Otherwise, clear contents first */ + else { + current->type = GUAC_CHAR_SET; + current->character = fill; + } + + current++; + + } + + } + /* Set width and height */ display->width = width; display->height = height; -} + /* Send display size */ + guac_common_surface_resize( + display->display_surface, + display->char_width * width, + display->char_height * height); + + guac_protocol_send_size(display->client->socket, + display->select_layer, + display->char_width * width, + display->char_height * height); -void guac_terminal_display_set_cursor(guac_terminal_display* display, - guac_terminal_cursor_type cursor) { - display->last_requested_cursor = cursor; } -void guac_terminal_display_render_buffer(guac_terminal_display* display, - guac_terminal_buffer* buffer, int scroll_offset, - guac_terminal_char* default_char, - bool cursor_visible, int cursor_row, int cursor_col, - bool text_selected, int selection_start_row, int selection_start_col, - int selection_end_row, int selection_end_col) { +void __guac_terminal_display_flush_copy(guac_terminal_display* display) { - if (selection_start_row > selection_end_row) { + guac_terminal_operation* current = display->operations; + int row, col; - int old_end_row = selection_end_row; - selection_end_row = selection_start_row; - selection_start_row = old_end_row; + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { - int old_end_col = selection_end_col; - selection_end_col = selection_start_col; - selection_start_col = old_end_col; + /* If operation is a copy operation */ + if (current->type == GUAC_CHAR_COPY) { + /* The determined bounds of the rectangle of contiguous + * operations */ + int detected_right = -1; + int detected_bottom = row; + + /* The current row or column within a rectangle */ + int rect_row, rect_col; + + /* The dimensions of the rectangle as determined */ + int rect_width, rect_height; + + /* The expected row and column source for the next copy + * operation (if adjacent to current) */ + int expected_row, expected_col; + + /* Current row within a subrect */ + guac_terminal_operation* rect_current_row; + + /* Determine bounds of rectangle */ + rect_current_row = current; + expected_row = current->row; + for (rect_row=row; rect_rowheight; rect_row++) { + + guac_terminal_operation* rect_current = rect_current_row; + expected_col = current->column; + + /* Find width */ + for (rect_col=col; rect_colwidth; rect_col++) { + + /* If not identical operation, stop */ + if (rect_current->type != GUAC_CHAR_COPY + || rect_current->row != expected_row + || rect_current->column != expected_col) + break; + + /* Next column */ + rect_current++; + expected_col++; + + } + + /* If too small, cannot append row */ + if (rect_col-1 < detected_right) + break; + + /* As row has been accepted, update rect_row of rect */ + detected_bottom = rect_row; + + /* For now, only set rect_col bound if uninitialized */ + if (detected_right == -1) + detected_right = rect_col - 1; + + /* Next row */ + rect_current_row += display->width; + expected_row++; + + } + + /* Calculate dimensions */ + rect_width = detected_right - col + 1; + rect_height = detected_bottom - row + 1; + + /* Mark rect as NOP (as it has been handled) */ + rect_current_row = current; + expected_row = current->row; + for (rect_row=0; rect_rowcolumn; + + for (rect_col=0; rect_coltype == GUAC_CHAR_COPY + && rect_current->row == expected_row + && rect_current->column == expected_col) + rect_current->type = GUAC_CHAR_NOP; + + /* Next column */ + rect_current++; + expected_col++; + + } + + /* Next row */ + rect_current_row += display->width; + expected_row++; + + } + + /* Send copy */ + guac_common_surface_copy( + + display->display_surface, + current->column * display->char_width, + current->row * display->char_height, + rect_width * display->char_width, + rect_height * display->char_height, + + display->display_surface, + col * display->char_width, + row * display->char_height); + + } /* end if copy operation */ + + /* Next operation */ + current++; + + } } - else if (selection_start_row == selection_end_row && selection_start_col > selection_end_col) { - int old_end_col = selection_end_col; - selection_end_col = selection_start_col; - selection_start_col = old_end_col; - } - if (display->current_cursor != display->last_requested_cursor) { +} + +void __guac_terminal_display_flush_clear(guac_terminal_display* display) { + + guac_terminal_operation* current = display->operations; + int row, col; + + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { + + /* If operation is a clear operation (set to space) */ + if (current->type == GUAC_CHAR_SET && + !guac_terminal_has_glyph(current->character.value)) { + + /* The determined bounds of the rectangle of contiguous + * operations */ + int detected_right = -1; + int detected_bottom = row; + + /* The current row or column within a rectangle */ + int rect_row, rect_col; + + /* The dimensions of the rectangle as determined */ + int rect_width, rect_height; + + /* Color of the rectangle to draw */ + guac_terminal_color color; + if (current->character.attributes.reverse != current->character.attributes.cursor) + color = current->character.attributes.foreground; + else + color = current->character.attributes.background; + + /* Rely only on palette index if defined */ + guac_terminal_display_lookup_color(display, + color.palette_index, &color); + + /* Current row within a subrect */ + guac_terminal_operation* rect_current_row; + + /* Determine bounds of rectangle */ + rect_current_row = current; + for (rect_row=row; rect_rowheight; rect_row++) { + + guac_terminal_operation* rect_current = rect_current_row; + + /* Find width */ + for (rect_col=col; rect_colwidth; rect_col++) { + + const guac_terminal_color* joining_color; + if (rect_current->character.attributes.reverse != rect_current->character.attributes.cursor) + joining_color = &rect_current->character.attributes.foreground; + else + joining_color = &rect_current->character.attributes.background; + + /* If not identical operation, stop */ + if (rect_current->type != GUAC_CHAR_SET + || guac_terminal_has_glyph(rect_current->character.value) + || guac_terminal_colorcmp(joining_color, &color) != 0) + break; + + /* Next column */ + rect_current++; + + } + + /* If too small, cannot append row */ + if (rect_col-1 < detected_right) + break; + + /* As row has been accepted, update rect_row of rect */ + detected_bottom = rect_row; + + /* For now, only set rect_col bound if uninitialized */ + if (detected_right == -1) + detected_right = rect_col - 1; + + /* Next row */ + rect_current_row += display->width; + + } + + /* Calculate dimensions */ + rect_width = detected_right - col + 1; + rect_height = detected_bottom - row + 1; + + /* Mark rect as NOP (as it has been handled) */ + rect_current_row = current; + for (rect_row=0; rect_rowcharacter.attributes.reverse != rect_current->character.attributes.cursor) + joining_color = &rect_current->character.attributes.foreground; + else + joining_color = &rect_current->character.attributes.background; + + /* Mark clear operations as NOP */ + if (rect_current->type == GUAC_CHAR_SET + && !guac_terminal_has_glyph(rect_current->character.value) + && guac_terminal_colorcmp(joining_color, &color) == 0) + rect_current->type = GUAC_CHAR_NOP; + + /* Next column */ + rect_current++; - switch (display->last_requested_cursor) { + } - case GUAC_TERMINAL_CURSOR_BLANK: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_NONE); - break; + /* Next row */ + rect_current_row += display->width; - case GUAC_TERMINAL_CURSOR_IBAR: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_IBAR); - break; + } - case GUAC_TERMINAL_CURSOR_POINTER: - guac_display_set_cursor(display->graphical_display, GUAC_DISPLAY_CURSOR_POINTER); - break; + /* Send rect */ + guac_common_surface_set( + display->display_surface, + col * display->char_width, + row * display->char_height, + rect_width * display->char_width, + rect_height * display->char_height, + color.red, color.green, color.blue, + 0xFF); + + } /* end if clear operation */ + + /* Next operation */ + current++; } + } + +} + +void __guac_terminal_display_flush_set(guac_terminal_display* display) { + + guac_terminal_operation* current = display->operations; + int row, col; + + /* For each operation */ + for (row=0; rowheight; row++) { + for (col=0; colwidth; col++) { - display->current_cursor = display->last_requested_cursor; + /* Perform given operation */ + if (current->type == GUAC_CHAR_SET) { + int codepoint = current->character.value; + + /* Use space if no glyph */ + if (!guac_terminal_has_glyph(codepoint)) + codepoint = ' '; + + /* Set attributes */ + __guac_terminal_set_colors(display, + &(current->character.attributes)); + + /* Send character */ + __guac_terminal_set(display, row, col, codepoint); + + /* Mark operation as handled */ + current->type = GUAC_CHAR_NOP; + + } + + /* Next operation */ + current++; + + } } - guac_display_layer_resize(display->display_layer, +} + +void guac_terminal_display_flush(guac_terminal_display* display) { + + /* Flush operations, copies first, then clears, then sets. */ + __guac_terminal_display_flush_copy(display); + __guac_terminal_display_flush_clear(display); + __guac_terminal_display_flush_set(display); + + /* Flush surface */ + guac_common_surface_flush(display->display_surface); + +} + +void guac_terminal_display_dup( + guac_terminal_display* display, guac_client* client, guac_socket* socket) { + + /* Create default surface */ + guac_common_surface_dup(display->display_surface, client, socket); + + /* Select layer is a child of the display layer */ + guac_protocol_send_move(socket, display->select_layer, + display->display_layer, 0, 0, 0); + + /* Offset the Default Layer to make margins even on all sides */ + guac_protocol_send_move(socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + + /* Send select layer size */ + guac_protocol_send_size(socket, display->select_layer, display->char_width * display->width, display->char_height * display->height); - /* Redraw region */ - for (int row = 0; row < display->height; row++) { +} - int adjusted_row = row - scroll_offset; +void guac_terminal_display_select(guac_terminal_display* display, + int start_row, int start_col, int end_row, int end_col) { - guac_terminal_char* characters; - unsigned int length = guac_terminal_buffer_get_columns(buffer, &characters, NULL, adjusted_row); + guac_socket* socket = display->client->socket; + guac_layer* select_layer = display->select_layer; - /* Copy characters */ - for (int col = 0; col < display->width; col++) { + /* Do nothing if selection is unchanged */ + if (display->text_selected + && display->selection_start_row == start_row + && display->selection_start_column == start_col + && display->selection_end_row == end_row + && display->selection_end_column == end_col) + return; - bool is_cursor = cursor_visible - && adjusted_row == cursor_row - && col == cursor_col; + /* Text is now selected */ + display->text_selected = true; - bool is_selected = text_selected - && adjusted_row >= selection_start_row - && adjusted_row <= selection_end_row - && (col >= selection_start_col || adjusted_row != selection_start_row) - && (col <= selection_end_col || adjusted_row != selection_end_row); + display->selection_start_row = start_row; + display->selection_start_column = start_col; + display->selection_end_row = end_row; + display->selection_end_column = end_col; - if (col < length) - guac_terminal_display_render_glyph(display, row, col, - &characters[col], is_cursor, is_selected); - else - guac_terminal_display_render_glyph(display, row, col, - default_char, is_cursor, is_selected); + /* If single row, just need one rectangle */ + if (start_row == end_row) { + /* Ensure proper ordering of columns */ + if (start_col > end_col) { + int temp = start_col; + start_col = end_col; + end_col = temp; } + /* Select characters between columns */ + guac_protocol_send_rect(socket, select_layer, + + start_col * display->char_width, + start_row * display->char_height, + + (end_col - start_col + 1) * display->char_width, + display->char_height); + } + /* Otherwise, need three */ + else { + + /* Ensure proper ordering of start and end coords */ + if (start_row > end_row) { + + int temp; + + temp = start_row; + start_row = end_row; + end_row = temp; + + temp = start_col; + start_col = end_col; + end_col = temp; + + } + + /* First row */ + guac_protocol_send_rect(socket, select_layer, + + start_col * display->char_width, + start_row * display->char_height, + + display->width * display->char_width, + display->char_height); + + /* Middle */ + guac_protocol_send_rect(socket, select_layer, + + 0, + (start_row + 1) * display->char_height, + + display->width * display->char_width, + (end_row - start_row - 1) * display->char_height); + + /* Last row */ + guac_protocol_send_rect(socket, select_layer, + + 0, + end_row * display->char_height, + + (end_col + 1) * display->char_width, + display->char_height); + + } + + /* Draw new selection, erasing old */ + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, + 0x00, 0x80, 0xFF, 0x60); + +} + +void guac_terminal_display_clear_select(guac_terminal_display* display) { + + /* Do nothing if nothing is selected */ + if (!display->text_selected) + return; + + guac_socket* socket = display->client->socket; + guac_layer* select_layer = display->select_layer; + + guac_protocol_send_rect(socket, select_layer, 0, 0, 1, 1); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer, + 0x00, 0x00, 0x00, 0x00); + + /* Text is no longer selected */ + display->text_selected = false; + } int guac_terminal_display_set_font(guac_terminal_display* display, diff --git a/src/terminal/scrollbar.c b/src/terminal/scrollbar.c index 8225365fb..9a89474b2 100644 --- a/src/terminal/scrollbar.c +++ b/src/terminal/scrollbar.c @@ -20,39 +20,23 @@ #include "terminal/scrollbar.h" #include -#include +#include #include +#include #include +#include -/** - * The opacity of the entire scrollbar, including both container and handle. The - * value 0x66 is 40% opacity. - */ -#define GUAC_TERMINAL_SCROLLBAR_OPACITY 0x66 - -/** - * The color to assign to the scrollbar handle (the component of the scrollbar - * that shows the current scroll position). - */ -#define GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR 0xFFFFFFFF - -/** - * The color to assign to the scrollbar container (the component of the - * scrollbar that contains the handle). - */ -#define GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR 0xFF808080 +#include guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - guac_display* graphical_display, guac_display_layer* parent, - int parent_width, int parent_height, int visible_area) { + const guac_layer* parent, int parent_width, int parent_height, int visible_area) { /* Allocate scrollbar */ guac_terminal_scrollbar* scrollbar = guac_mem_alloc(sizeof(guac_terminal_scrollbar)); - /* Associate client and corresponding display */ + /* Associate client */ scrollbar->client = client; - scrollbar->graphical_display = graphical_display; /* Init default min/max and value */ scrollbar->min = 0; @@ -78,17 +62,8 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, scrollbar->render_state.container_height = 0; /* Allocate and init layers */ - scrollbar->container = guac_display_alloc_layer(graphical_display, 1); - scrollbar->handle = guac_display_alloc_layer(graphical_display, 1); - - /* The parent layer contains the scrollbar container, while the container - * layer contains the scrollbar handle */ - guac_display_layer_set_parent(scrollbar->container, scrollbar->parent); - guac_display_layer_set_parent(scrollbar->handle, scrollbar->container); - - /* Use layer-level transparency to blend the scrollbar with the background - * color, rather than graphical updates leveraging the alpha channel */ - guac_display_layer_set_opacity(scrollbar->container, GUAC_TERMINAL_SCROLLBAR_OPACITY); + scrollbar->container = guac_client_alloc_layer(client); + scrollbar->handle = guac_client_alloc_layer(client); /* Init mouse event state tracking */ scrollbar->dragging_handle = 0; @@ -104,8 +79,8 @@ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /* Free layers */ - guac_display_free_layer(scrollbar->handle); - guac_display_free_layer(scrollbar->container); + guac_client_free_layer(scrollbar->client, scrollbar->handle); + guac_client_free_layer(scrollbar->client, scrollbar->container); /* Free scrollbar */ guac_mem_free(scrollbar); @@ -114,8 +89,8 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { /** * Moves the main scrollbar layer to the position indicated within the given - * scrollbar render state, updating the underlying Guacamole display such that - * the new position will be sent to connected users for the next frame. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. * * @param scrollbar * The scrollbar to reposition. @@ -123,20 +98,29 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) { * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * position. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_move_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { - guac_display_layer_move(scrollbar->container, - state->container_x, state->container_y); + /* Send scrollbar position */ + guac_protocol_send_move(socket, + scrollbar->container, scrollbar->parent, + state->container_x, + state->container_y, + 0); } /** * Resizes and redraws the main scrollbar layer according to the given - * scrollbar render state, updating the underlying Guacamole display such that - * the new position will be sent to connected users for the next frame. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. * * @param scrollbar * The scrollbar to resize and redraw. @@ -144,40 +128,37 @@ static void guac_terminal_scrollbar_move_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * size and appearance. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_draw_container( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { /* Set container size */ - guac_display_layer_resize(scrollbar->container, - state->container_width, state->container_height); + guac_protocol_send_size(socket, scrollbar->container, + state->container_width, + state->container_height); /* Fill container with solid color */ + guac_protocol_send_rect(socket, scrollbar->container, 0, 0, + state->container_width, + state->container_height); - guac_rect rect = { - .left = 0, - .top = 0, - .right = state->container_width, - .bottom = state->container_height - }; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->container); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_CONTAINER_COLOR); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(scrollbar->container, context); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->container, + 0x80, 0x80, 0x80, 0x40); } /** * Moves the handle layer of the scrollbar to the position indicated within the - * given scrollbar render state, updating the underlying Guacamole display such - * that the new position will be sent to connected users for the next frame. The - * handle is the portion of the scrollbar that indicates the current scroll - * value and which the user can click and drag to change the value. + * given scrollbar render state, sending any necessary Guacamole instructions + * over the given socket. The handle is the portion of the scrollbar that + * indicates the current scroll value and which the user can click and drag to + * change the value. * * @param scrollbar * The scrollbar associated with the handle being repositioned. @@ -185,21 +166,31 @@ static void guac_terminal_scrollbar_draw_container( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle position. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_move_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { - guac_display_layer_move(scrollbar->handle, state->handle_x, state->handle_y); + /* Send handle position */ + guac_protocol_send_move(socket, + scrollbar->handle, scrollbar->container, + state->handle_x, + state->handle_y, + 0); } /** * Resizes and redraws the handle layer of the scrollbar according to the given - * scrollbar render state, updating the underlying Guacamole display such - * that the new position will be sent to connected users for the next frame. The - * handle is the portion of the scrollbar that indicates the current scroll - * value and which the user can click and drag to change the value. + * scrollbar render state, sending any necessary Guacamole instructions over + * the given socket. The handle is the portion of the scrollbar that indicates + * the current scroll value and which the user can click and drag to change the + * value. * * @param scrollbar * The scrollbar associated with the handle being resized and redrawn. @@ -207,31 +198,28 @@ static void guac_terminal_scrollbar_move_handle( * @param state * The guac_terminal_scrollbar_render_state describing the new scrollbar * handle size and appearance. + * + * @param socket + * The guac_socket over which any instructions necessary to perform the + * render operation should be sent. */ static void guac_terminal_scrollbar_draw_handle( guac_terminal_scrollbar* scrollbar, - guac_terminal_scrollbar_render_state* state) { + guac_terminal_scrollbar_render_state* state, + guac_socket* socket) { /* Set handle size */ - guac_display_layer_resize(scrollbar->handle, - state->handle_width, state->handle_height); + guac_protocol_send_size(socket, scrollbar->handle, + state->handle_width, + state->handle_height); /* Fill handle with solid color */ + guac_protocol_send_rect(socket, scrollbar->handle, 0, 0, + state->handle_width, + state->handle_height); - guac_rect rect = { - .left = 0, - .top = 0, - .right = state->handle_width, - .bottom = state->handle_height - }; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(scrollbar->handle); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, GUAC_TERMINAL_SCROLLBAR_HANDLE_COLOR); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(scrollbar->handle, context); + guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->handle, + 0xA0, 0xA0, 0xA0, 0x8F); } @@ -345,8 +333,26 @@ static void calculate_state(guac_terminal_scrollbar* scrollbar, } +void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, + guac_client* client, guac_socket* socket) { + + /* Get old state */ + guac_terminal_scrollbar_render_state* state = &scrollbar->render_state; + + /* Send scrollbar container */ + guac_terminal_scrollbar_draw_container(scrollbar, state, socket); + guac_terminal_scrollbar_move_container(scrollbar, state, socket); + + /* Send handle */ + guac_terminal_scrollbar_draw_handle(scrollbar, state, socket); + guac_terminal_scrollbar_move_handle(scrollbar, state, socket); + +} + void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { + guac_socket* socket = scrollbar->client->socket; + /* Get old state */ int old_value = scrollbar->value; guac_terminal_scrollbar_render_state* old_state = &scrollbar->render_state; @@ -363,25 +369,25 @@ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) { /* Reposition container if moved */ if (old_state->container_x != new_state.container_x || old_state->container_y != new_state.container_y) { - guac_terminal_scrollbar_move_container(scrollbar, &new_state); + guac_terminal_scrollbar_move_container(scrollbar, &new_state, socket); } /* Resize and redraw container if size changed */ if (old_state->container_width != new_state.container_width || old_state->container_height != new_state.container_height) { - guac_terminal_scrollbar_draw_container(scrollbar, &new_state); + guac_terminal_scrollbar_draw_container(scrollbar, &new_state, socket); } /* Reposition handle if moved */ if (old_state->handle_x != new_state.handle_x || old_state->handle_y != new_state.handle_y) { - guac_terminal_scrollbar_move_handle(scrollbar, &new_state); + guac_terminal_scrollbar_move_handle(scrollbar, &new_state, socket); } /* Resize and redraw handle if size changed */ if (old_state->handle_width != new_state.handle_width || old_state->handle_height != new_state.handle_height) { - guac_terminal_scrollbar_draw_handle(scrollbar, &new_state); + guac_terminal_scrollbar_draw_handle(scrollbar, &new_state, socket); } /* Store current render state */ diff --git a/src/terminal/select.c b/src/terminal/select.c index 62c03dc3f..406a0c43f 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -76,7 +76,7 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, *start_row = terminal->selection_start_row; *start_col = terminal->selection_start_column; *end_row = terminal->selection_end_row; - *end_col = terminal->selection_end_column; + *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; } @@ -84,21 +84,107 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, * final character width */ else { *end_row = terminal->selection_start_row; - *end_col = terminal->selection_start_column; + *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; *start_row = terminal->selection_end_row; *start_col = terminal->selection_end_column; } } +void guac_terminal_select_redraw(guac_terminal* terminal) { + + /* Update the selected region of the display if text is currently + * selected */ + if (terminal->text_selected) { + + int start_row = terminal->selection_start_row + terminal->scroll_offset; + int start_column = terminal->selection_start_column; + + int end_row = terminal->selection_end_row + terminal->scroll_offset; + int end_column = terminal->selection_end_column; + + /* Update start/end columns to include character width */ + if (start_row > end_row || (start_row == end_row && start_column > end_column)) + start_column += terminal->selection_start_width - 1; + else + end_column += terminal->selection_end_width - 1; + + guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); + + } + + /* Clear the display selection if no text is currently selected */ + else + guac_terminal_display_clear_select(terminal->display); + +} + +/** + * Locates the beginning of the character at the given row and column, updating + * the column to the starting column of that character. The width, if available, + * is returned. If the character has no defined width, 1 is returned. + * + * @param terminal + * The guac_terminal in which the character should be located. + * + * @param row + * The row number of the desired character, 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 column + * A pointer to an int containing the column number of the desired + * character, where 0 is the first (left-most) column within the row. If + * the character is a multi-column character, the value of this int will be + * adjusted as necessary such that it contains the column number of the + * first column containing the character. + * + * @return + * The width of the specified character, in columns, or 1 if the character + * has no defined width. + */ +static int guac_terminal_find_char(guac_terminal* terminal, + int row, int* column) { + + guac_terminal_char* characters; + int start_column = *column; + + 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 = &(characters[start_column]); + while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { + start_char--; + start_column--; + } + + /* Use width, if available */ + if (start_char->value != GUAC_CHAR_CONTINUATION) { + *column = start_column; + return start_char->width; + } + + } + + /* Default to one column wide */ + return 1; + +} + void guac_terminal_select_start(guac_terminal* terminal, int row, int column) { + int width = guac_terminal_find_char(terminal, row, &column); + terminal->selection_start_row = terminal->selection_end_row = row; terminal->selection_start_column = terminal->selection_end_column = column; + terminal->selection_start_width = + terminal->selection_end_width = width; + terminal->text_selected = false; terminal->selection_committed = false; guac_terminal_notify(terminal); @@ -109,11 +195,14 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { /* Only update if selection has changed */ if (row != terminal->selection_end_row - || column <= terminal->selection_end_column - || column >= terminal->selection_end_column) { + || column <= terminal->selection_end_column + || column >= terminal->selection_end_column + terminal->selection_end_width) { + + int width = guac_terminal_find_char(terminal, row, &column); terminal->selection_end_row = row; terminal->selection_end_column = column; + terminal->selection_end_width = width; terminal->text_selected = true; guac_terminal_notify(terminal); diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 9d8e4ff99..d98e6117a 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -18,6 +18,7 @@ */ #include "common/clipboard.h" +#include "common/cursor.h" #include "common/iconv.h" #include "terminal/buffer.h" #include "terminal/color-scheme.h" @@ -33,7 +34,6 @@ #include #include -#include #include #include #include @@ -53,6 +53,23 @@ #include #include +/** + * Sets the given range of columns to the given character. + */ +static void __guac_terminal_set_columns(guac_terminal* terminal, int row, + int start_column, int end_column, guac_terminal_char* character) { + + guac_terminal_display_set_columns(terminal->display, row + terminal->scroll_offset, + start_column, end_column, character); + + guac_terminal_buffer_set_columns(terminal->current_buffer, row, + start_column, end_column, character); + + /* Clear selection if region is modified */ + guac_terminal_select_touch(terminal, row, start_column, row, end_column); + +} + /** * Returns the number of rows available within the terminal buffer, taking * changes to the desired scrollback size into account. Regardless of the @@ -79,6 +96,8 @@ static int guac_terminal_effective_buffer_length(guac_terminal* term) { else if (scrollback < term->term_height) scrollback = term->term_height; + /* If the buffer contains more rows than requested, pretend it only + * contains the requested number of rows */ return guac_terminal_buffer_effective_length(term->current_buffer, scrollback); } @@ -106,8 +125,8 @@ void guac_terminal_reset(guac_terminal* term) { term->char_mapping[1] = NULL; /* Reset cursor location */ - term->cursor_row = term->saved_cursor_row = 0; - term->cursor_col = term->saved_cursor_col = 0; + term->cursor_row = term->visible_cursor_row = term->saved_cursor_row = 0; + term->cursor_col = term->visible_cursor_col = term->saved_cursor_col = 0; term->cursor_visible = true; /* Clear scrollback, buffer, and scroll region */ @@ -153,37 +172,28 @@ void guac_terminal_reset(guac_terminal* term) { * * @param terminal * The terminal whose background should be painted or repainted. + * + * @param socket + * The socket over which instructions required to paint / repaint the + * terminal background should be send. */ -static void guac_terminal_repaint_default_layer(guac_terminal* terminal) { +static void guac_terminal_repaint_default_layer(guac_terminal* terminal, + guac_socket* socket) { int width = terminal->width; int height = terminal->height; guac_terminal_display* display = terminal->display; - guac_display_layer* default_layer = guac_display_default_layer(terminal->graphical_display); + + /* Get background color */ + const guac_terminal_color* color = &display->default_background; /* Reset size */ - guac_display_layer_resize(default_layer, width, height); + guac_protocol_send_size(socket, GUAC_DEFAULT_LAYER, width, height); /* Paint background color */ - guac_rect rect = { - .left = 0, - .top = 0, - .right = width, - .bottom = height - }; - - uint32_t background = 0xFF000000 - | (display->default_background.red << 16) - | (display->default_background.green << 8) - | display->default_background.blue; - - guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); - - guac_rect_constrain(&rect, &context->bounds); - guac_display_layer_raw_context_set(context, &rect, background); - guac_rect_extend(&context->dirty, &rect); - - guac_display_layer_close_raw(default_layer, context); + guac_protocol_send_rect(socket, GUAC_DEFAULT_LAYER, 0, 0, width, height); + guac_protocol_send_cfill(socket, GUAC_COMP_OVER, GUAC_DEFAULT_LAYER, + color->red, color->green, color->blue, 0xFF); } @@ -210,6 +220,10 @@ void* guac_terminal_thread(void* data) { if (guac_terminal_render_frame(terminal)) break; + /* Signal end of frame */ + guac_client_end_frame(client); + guac_socket_flush(client->socket); + } /* The client has stopped or an error has occurred */ @@ -386,8 +400,7 @@ guac_terminal* guac_terminal_create(guac_client* client, term->alternate_buffer = guac_terminal_buffer_alloc(GUAC_TERMINAL_MAX_ROWS, &default_char); /* Init display */ - term->graphical_display = guac_display_alloc(client); - term->display = guac_terminal_display_alloc(client, term->graphical_display, + term->display = guac_terminal_display_alloc(client, options->font_name, options->font_size, options->dpi, &default_char.attributes.foreground, &default_char.attributes.background, @@ -400,6 +413,9 @@ guac_terminal* guac_terminal_create(guac_client* client, return NULL; } + /* Init common cursor */ + term->cursor = guac_common_cursor_alloc(client); + /* Init terminal state */ term->current_attributes = default_char.attributes; term->default_char = default_char; @@ -449,13 +465,12 @@ guac_terminal* guac_terminal_create(guac_client* client, pthread_mutex_init(&(term->lock), NULL); /* Repaint and resize overall display */ - guac_terminal_repaint_default_layer(term); + guac_terminal_repaint_default_layer(term, term->client->socket); guac_terminal_display_resize(term->display, term->term_width, term->term_height); /* Allocate scrollbar */ - term->scrollbar = guac_terminal_scrollbar_alloc(term->client, term->graphical_display, - guac_display_default_layer(term->graphical_display), + term->scrollbar = guac_terminal_scrollbar_alloc(term->client, GUAC_DEFAULT_LAYER, term->outer_width, term->outer_height, term->term_height); /* Associate scrollbar with this terminal */ @@ -474,6 +489,10 @@ guac_terminal* guac_terminal_create(guac_client* client, term->mod_meta = term->mod_shift = 0; + /* Initialize mouse cursor */ + term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; + guac_common_cursor_set_blank(term->cursor); + /* Start terminal thread */ if (pthread_create(&(term->thread), NULL, guac_terminal_thread, (void*) term)) { @@ -529,7 +548,6 @@ void guac_terminal_free(guac_terminal* term) { /* Free display */ guac_terminal_display_free(term->display); - guac_display_free(term->graphical_display); /* Free buffers */ guac_terminal_buffer_free(term->normal_buffer); @@ -733,6 +751,51 @@ int guac_terminal_set(guac_terminal* term, int row, int col, int codepoint) { } +void guac_terminal_commit_cursor(guac_terminal* term) { + + /* 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) { + + 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) { + + 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 */ + else { + term->visible_cursor_row = -1; + term->visible_cursor_col = -1; + } + + return; + +} + int guac_terminal_write(guac_terminal* term, const char* buffer, int length) { guac_terminal_lock(term); @@ -768,6 +831,9 @@ void guac_terminal_scroll_up(guac_terminal* term, /* If scrolling entire display, update scroll offset */ if (start_row == 0 && end_row == term->term_height - 1) { + /* Scroll up visibly */ + guac_terminal_display_copy_rows(term->display, start_row + amount, end_row, -amount); + /* Advance by scroll amount */ guac_terminal_buffer_scroll_up(term->current_buffer, amount); @@ -775,6 +841,11 @@ void guac_terminal_scroll_up(guac_terminal* term, guac_terminal_scrollbar_set_bounds(term->scrollbar, -guac_terminal_get_available_scroll(term), 0); + /* Update cursor location if within region */ + if (term->visible_cursor_row >= start_row && + term->visible_cursor_row <= end_row) + term->visible_cursor_row -= amount; + /* Update selected region */ if (term->text_selected) { term->selection_start_row -= amount; @@ -792,6 +863,10 @@ void guac_terminal_scroll_up(guac_terminal* term, end_row - amount + 1, 0, end_row, term->term_width - 1); + /* Flush display copy before the cursor commit override operation + * type for visible cursor row and breaks display. */ + guac_terminal_display_flush(term->display); + } void guac_terminal_scroll_down(guac_terminal* term, @@ -804,6 +879,10 @@ void guac_terminal_scroll_down(guac_terminal* term, start_row, 0, start_row + amount - 1, term->term_width - 1); + /* Flush display copy before the cursor commit override operation + * type for visible cursor row and breaks display. */ + guac_terminal_display_flush(term->display); + } int guac_terminal_clear_columns(guac_terminal* term, @@ -866,9 +945,53 @@ int guac_terminal_clear_range(guac_terminal* term, } +/** + * Returns whether the given character would be visible relative to the + * background of the given terminal. + * + * @param term + * The guac_terminal to test the character against. + * + * @param c + * The character being tested. + * + * @return + * true if the given character is different from the terminal background, + * false otherwise. + */ +static bool guac_terminal_is_visible(guac_terminal* term, + guac_terminal_char* c) { + + /* Continuation characters are NEVER visible */ + if (c->value == GUAC_CHAR_CONTINUATION) + return false; + + /* Characters with glyphs are ALWAYS visible */ + if (guac_terminal_has_glyph(c->value)) + return true; + + const guac_terminal_color* background; + + /* Determine actual background color of character */ + if (c->attributes.reverse != c->attributes.cursor) + background = &c->attributes.foreground; + else + background = &c->attributes.background; + + /* Blank characters are visible if their background color differs from that + * of the terminal */ + return guac_terminal_colorcmp(background, + &term->default_char.attributes.background) != 0; + +} + void guac_terminal_scroll_display_down(guac_terminal* terminal, int scroll_amount) { + int start_row, end_row; + int dest_row; + int row, column; + /* Limit scroll amount by size of scrollback buffer */ if (scroll_amount > terminal->scroll_offset) scroll_amount = terminal->scroll_offset; @@ -877,10 +1000,49 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, if (scroll_amount <= 0) return; + /* Shift screen up */ + if (terminal->term_height > scroll_amount) + guac_terminal_display_copy_rows(terminal->display, + scroll_amount, terminal->term_height - 1, + -scroll_amount); + /* Advance by scroll amount */ terminal->scroll_offset -= scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); + /* Get row range */ + end_row = terminal->term_height - terminal->scroll_offset - 1; + start_row = end_row - scroll_amount + 1; + dest_row = terminal->term_height - scroll_amount; + + /* Draw new rows from scrollback */ + for (row=start_row; row<=end_row; row++) { + + /* Get row from scrollback */ + 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 = characters; + for (column = 0; column < length; column++) { + + /* Only draw if not blank */ + if (guac_terminal_is_visible(terminal, current)) + guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); + + current++; + + } + + /* Next row */ + dest_row++; + + } + guac_terminal_notify(terminal); } @@ -888,6 +1050,10 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, void guac_terminal_scroll_display_up(guac_terminal* terminal, int scroll_amount) { + int start_row, end_row; + int dest_row; + int row, column; + /* Limit scroll amount by size of scrollback buffer */ int available_scroll = guac_terminal_get_available_scroll(terminal); if (terminal->scroll_offset + scroll_amount > available_scroll) @@ -897,10 +1063,49 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, if (scroll_amount <= 0) return; + /* Shift screen down */ + if (terminal->term_height > scroll_amount) + guac_terminal_display_copy_rows(terminal->display, + 0, terminal->term_height - scroll_amount - 1, + scroll_amount); + /* Advance by scroll amount */ terminal->scroll_offset += scroll_amount; guac_terminal_scrollbar_set_value(terminal->scrollbar, -terminal->scroll_offset); + /* Get row range */ + start_row = -terminal->scroll_offset; + end_row = start_row + scroll_amount - 1; + dest_row = 0; + + /* Draw new rows from scrollback */ + for (row=start_row; row<=end_row; row++) { + + /* Get row from scrollback */ + 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 = characters; + for (column = 0; column < length; column++) { + + /* Only draw if not blank */ + if (guac_terminal_is_visible(terminal, current)) + guac_terminal_display_set_columns(terminal->display, dest_row, column, column, current); + + current++; + + } + + /* Next row */ + dest_row++; + + } + guac_terminal_notify(terminal); } @@ -908,17 +1113,29 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, void guac_terminal_copy_columns(guac_terminal* terminal, int row, int start_column, int end_column, int offset) { + guac_terminal_display_copy_columns(terminal->display, row + terminal->scroll_offset, + start_column, end_column, offset); + guac_terminal_buffer_copy_columns(terminal->current_buffer, row, start_column, end_column, offset); /* Clear selection if region is modified */ guac_terminal_select_touch(terminal, row, start_column, row, end_column); + /* Update cursor location if within region */ + if (row == terminal->visible_cursor_row && + terminal->visible_cursor_col >= start_column && + terminal->visible_cursor_col <= end_column) + terminal->visible_cursor_col += offset; + } void guac_terminal_copy_rows(guac_terminal* terminal, int start_row, int end_row, int offset) { + guac_terminal_display_copy_rows(terminal->display, + start_row + terminal->scroll_offset, end_row + terminal->scroll_offset, offset); + guac_terminal_buffer_copy_rows(terminal->current_buffer, start_row, end_row, offset); @@ -926,16 +1143,59 @@ void guac_terminal_copy_rows(guac_terminal* terminal, guac_terminal_select_touch(terminal, start_row, 0, end_row, terminal->term_width); + /* Update cursor location if within region */ + if (terminal->visible_cursor_row >= start_row && + terminal->visible_cursor_row <= end_row) + terminal->visible_cursor_row += offset; + } void guac_terminal_set_columns(guac_terminal* terminal, int row, int start_column, int end_column, guac_terminal_char* character) { - guac_terminal_buffer_set_columns(terminal->current_buffer, row, - start_column, end_column, character); + __guac_terminal_set_columns(terminal, row, start_column, end_column, character); - /* Clear selection if region is modified */ - guac_terminal_select_touch(terminal, row, start_column, row, end_column); + /* If visible cursor in current row, preserve state */ + if (row == terminal->visible_cursor_row + && terminal->visible_cursor_col >= start_column + && terminal->visible_cursor_col <= end_column) { + + /* Create copy of character with cursor attribute set */ + guac_terminal_char cursor_character = *character; + cursor_character.attributes.cursor = true; + + __guac_terminal_set_columns(terminal, row, + terminal->visible_cursor_col, terminal->visible_cursor_col, &cursor_character); + + } + +} + +static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int start_col, int end_row, int end_col) { + + int row, col; + + /* Redraw region */ + for (row=start_row; row<=end_row; row++) { + + 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 < length; col++) { + + /* Only redraw if not blank */ + guac_terminal_char* c = &characters[col]; + if (guac_terminal_is_visible(term, c)) + guac_terminal_display_set_columns(term->display, row, col, col, c); + + } + + } } @@ -969,17 +1229,30 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { /* If the new terminal bottom covers N rows, shift up N rows */ if (shift_amount > 0) { + guac_terminal_display_copy_rows(term->display, + shift_amount, term->display->height - 1, -shift_amount); + /* Update buffer top and cursor row based on shift */ guac_terminal_buffer_scroll_up(term->current_buffer, shift_amount); - term->cursor_row -= shift_amount; + term->cursor_row -= shift_amount; + if (term->visible_cursor_row != -1) + term->visible_cursor_row -= shift_amount; + + /* Redraw characters within old region */ + __guac_terminal_redraw_rect(term, height - shift_amount, 0, height-1, width-1); } } /* Resize display */ + guac_terminal_display_flush(term->display); guac_terminal_display_resize(term->display, width, height); + /* Redraw any characters on right if widening */ + if (width > term->term_width) + __guac_terminal_redraw_rect(term, 0, term->term_width-1, height-1, width-1); + /* If height is increasing, shift display down */ if (height > term->term_height) { @@ -996,36 +1269,66 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { /* Update buffer top and cursor row based on shift */ guac_terminal_buffer_scroll_down(term->current_buffer, shift_amount); - term->cursor_row += shift_amount; + term->cursor_row += shift_amount; + if (term->visible_cursor_row != -1) + term->visible_cursor_row += shift_amount; /* If scrolled enough, use scroll to fulfill entire resize */ if (term->scroll_offset >= shift_amount) { + term->scroll_offset -= shift_amount; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); + + /* Draw characters from scroll at bottom */ + __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + shift_amount - 1, width-1); + } /* Otherwise, fulfill with as much scroll as possible */ else { + + /* Draw characters from scroll at bottom */ + __guac_terminal_redraw_rect(term, term->term_height, 0, term->term_height + term->scroll_offset - 1, width-1); + + /* Update shift_amount and scroll based on new rows */ shift_amount -= term->scroll_offset; term->scroll_offset = 0; guac_terminal_scrollbar_set_value(term->scrollbar, -term->scroll_offset); + + /* If anything remains, move screen as necessary */ + if (shift_amount > 0) { + + guac_terminal_display_copy_rows(term->display, + 0, term->display->height - shift_amount - 1, shift_amount); + + /* Draw characters at top from scroll */ + __guac_terminal_redraw_rect(term, 0, 0, shift_amount - 1, width-1); + + } + } } /* end if undisplayed rows exist */ } + /* Keep cursor on screen */ + if (term->cursor_row < 0) term->cursor_row = 0; + if (term->cursor_row >= height) term->cursor_row = height-1; + if (term->cursor_col < 0) term->cursor_col = 0; + if (term->cursor_col >= width) term->cursor_col = width-1; + /* Commit new dimensions */ term->term_width = width; term->term_height = height; - /* Reset scroll region */ - term->scroll_end = height - 1; - } int guac_terminal_resize(guac_terminal* terminal, int width, int height) { + guac_terminal_display* display = terminal->display; + guac_client* client = display->client; + /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1048,11 +1351,15 @@ int guac_terminal_resize(guac_terminal* terminal, int width, int height) { terminal->width = adjusted_width; /* Resize default layer to given pixel dimensions */ - guac_terminal_repaint_default_layer(terminal); + guac_terminal_repaint_default_layer(terminal, client->socket); /* Resize terminal if row/column dimensions have changed */ if (columns != terminal->term_width || rows != terminal->term_height) { + /* Resize terminal and set the columns and rows on the terminal struct */ __guac_terminal_resize(terminal, columns, rows); + + /* Reset scroll region */ + terminal->scroll_end = rows - 1; } /* Notify scrollbar of resize */ @@ -1080,18 +1387,11 @@ void guac_terminal_flush(guac_terminal* terminal) { guac_terminal_pipe_stream_flush(terminal); /* Flush display state */ + guac_terminal_select_redraw(terminal); + guac_terminal_commit_cursor(terminal); + guac_terminal_display_flush(terminal->display); guac_terminal_scrollbar_flush(terminal->scrollbar); - guac_terminal_display_render_buffer(terminal->display, - terminal->current_buffer, terminal->scroll_offset, - &terminal->default_char, - terminal->cursor_visible, terminal->cursor_row, terminal->cursor_col, - terminal->text_selected, terminal->selection_start_row, terminal->selection_start_column, - terminal->selection_end_row, terminal->selection_end_column); - - guac_display_end_multiple_frames(terminal->graphical_display, 1); - guac_socket_flush(terminal->client->socket); - } void guac_terminal_lock(guac_terminal* terminal) { @@ -1132,8 +1432,11 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed } /* Hide mouse cursor if not already hidden */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_BLANK); - guac_terminal_notify(term); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_BLANK) { + term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; + guac_common_cursor_set_blank(term->cursor); + guac_terminal_notify(term); + } /* Track modifiers */ if (keysym == 0xFFE3 || keysym == 0xFFE4) @@ -1438,13 +1741,17 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int pressed_mask = ~term->mouse_mask & mask; /* Store current mouse location/state */ - guac_display_notify_user_moved_mouse(term->graphical_display, user, x, y, mask); + guac_common_cursor_update(term->cursor, user, x, y, mask); /* Notify scrollbar, do not handle anything handled by scrollbar */ if (guac_terminal_scrollbar_handle_mouse(term->scrollbar, x, y, mask)) { /* Set pointer cursor if mouse is over scrollbar */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_POINTER); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_POINTER) { + term->current_cursor = GUAC_TERMINAL_CURSOR_POINTER; + guac_common_cursor_set_pointer(term->cursor); + guac_terminal_notify(term); + } guac_terminal_notify(term); return 0; @@ -1458,8 +1765,11 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, term->mouse_mask = mask; /* Show mouse cursor if not already shown */ - guac_terminal_display_set_cursor(term->display, GUAC_TERMINAL_CURSOR_IBAR); - guac_terminal_notify(term); + if (term->current_cursor != GUAC_TERMINAL_CURSOR_IBAR) { + term->current_cursor = GUAC_TERMINAL_CURSOR_IBAR; + guac_common_cursor_set_ibar(term->cursor); + guac_terminal_notify(term); + } /* Paste contents of clipboard on right or middle mouse button up */ if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE)) @@ -1752,19 +2062,47 @@ int guac_terminal_create_typescript(guac_terminal* term, const char* path, } +/** + * Synchronize the state of the provided terminal to a subset of users of + * the provided guac_client using the provided socket. + * + * @param client + * The client whose users should be synchronized. + * + * @param term + * The terminal state that should be synchronized to the users. + * + * @param socket + * The socket that should be used to communicate with the users. + */ +static void __guac_terminal_sync_socket( + guac_client* client, guac_terminal* term, guac_socket* socket) { + + /* Synchronize display state with new user */ + guac_terminal_repaint_default_layer(term, socket); + guac_terminal_display_dup(term->display, client, socket); + + /* Synchronize mouse cursor */ + guac_common_cursor_dup(term->cursor, client, socket); + + /* Paint scrollbar for joining users */ + guac_terminal_scrollbar_dup(term->scrollbar, client, socket); + +} + void guac_terminal_dup(guac_terminal* term, guac_user* user, guac_socket* socket) { - /* Synchronize state to any users on given socket */ - guac_display_dup(term->graphical_display, socket); + /* Ignore the user and just use the provided socket directly */ + __guac_terminal_sync_socket(user->client, term, socket); } -void guac_terminal_sync_users(guac_terminal* term, guac_client* client, - guac_socket* socket) { +void guac_terminal_sync_users( + guac_terminal* term, guac_client* client, guac_socket* socket) { - /* Synchronize state to any users on given socket */ - guac_display_dup(term->graphical_display, socket); + /* Use the provided socket to synchronize state to the users */ + __guac_terminal_sync_socket(client, term, socket); } @@ -1787,7 +2125,10 @@ void guac_terminal_apply_color_scheme(guac_terminal* terminal, display->default_background = default_char->attributes.background; /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal); + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1810,6 +2151,7 @@ const char* guac_terminal_get_color_scheme(guac_terminal* terminal) { void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, int font_size, int dpi) { + guac_client* client = terminal->client; guac_terminal_display* display = terminal->display; if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) @@ -1820,6 +2162,12 @@ void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, guac_terminal_resize(terminal, terminal->outer_width, terminal->outer_height); + /* Redraw terminal text and background */ + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); + /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -1880,5 +2228,7 @@ void guac_terminal_clipboard_append(guac_terminal* terminal, } void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) { - guac_display_notify_user_left(terminal->graphical_display, user); + + /* Remove the user from the terminal cursor */ + guac_common_cursor_remove_user(terminal->cursor, user); } diff --git a/src/terminal/terminal/buffer.h b/src/terminal/terminal/buffer.h index 2e974d6a5..9f175ba14 100644 --- a/src/terminal/terminal/buffer.h +++ b/src/terminal/terminal/buffer.h @@ -168,5 +168,23 @@ unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, */ 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/display.h b/src/terminal/terminal/display.h index 30b4e9e02..635536088 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -26,13 +26,11 @@ * @file display.h */ -#include "buffer.h" +#include "common/surface.h" #include "palette.h" -#include "terminal.h" #include "types.h" #include -#include #include #include @@ -56,18 +54,74 @@ #define GUAC_TERMINAL_MM_PER_INCH 25.4 /** - * The rendering area and state of the text display used by the terminal - * emulator. The actual changes between successive frames are tracked by an - * underlying guac_display. + * All available terminal operations which affect character cells. + */ +typedef enum guac_terminal_operation_type { + + /** + * Operation which does nothing. + */ + GUAC_CHAR_NOP, + + /** + * Operation which copies a character from a given row/column coordinate. + */ + GUAC_CHAR_COPY, + + /** + * Operation which sets the character and attributes. + */ + GUAC_CHAR_SET + +} guac_terminal_operation_type; + +/** + * A pairing of a guac_terminal_operation_type and all parameters required by + * that operation type. + */ +typedef struct guac_terminal_operation { + + /** + * The type of operation to perform. + */ + guac_terminal_operation_type type; + + /** + * The character (and attributes) to set the current location to. This is + * only applicable to GUAC_CHAR_SET. + */ + guac_terminal_char character; + + /** + * The row to copy a character from. This is only applicable to + * GUAC_CHAR_COPY. + */ + int row; + + /** + * The column to copy a character from. This is only applicable to + * GUAC_CHAR_COPY. + */ + int column; + +} guac_terminal_operation; + +/** + * Set of all pending operations for the currently-visible screen area, and the + * contextual information necessary to interpret and render those changes. */ typedef struct guac_terminal_display { /** - * The Guacamole client associated with this terminal emulator having this - * display. + * The Guacamole client this display will use for rendering. */ guac_client* client; + /** + * Array of all operations pending for the visible screen area. + */ + guac_terminal_operation* operations; + /** * The width of the screen, in characters. */ @@ -83,18 +137,6 @@ typedef struct guac_terminal_display { */ int margin; - /** - * The current mouse cursor (the mouse cursor already sent to connected - * users), to avoid re-setting the cursor image when effectively no change - * has been made. - */ - guac_terminal_cursor_type current_cursor; - - /** - * The mouse cursor that was most recently requested. - */ - guac_terminal_cursor_type last_requested_cursor; - /** * The description of the font to use for rendering. */ @@ -132,50 +174,64 @@ typedef struct guac_terminal_display { guac_terminal_color default_background; /** - * The Guacamole display that this terminal emulator should render to. + * The foreground color to be used for the next glyph rendered to the + * terminal. + */ + guac_terminal_color glyph_foreground; + + /** + * The background color to be used for the next glyph rendered to the + * terminal. + */ + guac_terminal_color glyph_background; + + /** + * The surface containing the actual terminal. */ - guac_display* graphical_display; + guac_common_surface* display_surface; /** * Layer which contains the actual terminal. */ - guac_display_layer* display_layer; + guac_layer* display_layer; + + /** + * Sub-layer of display layer which highlights selected text. + */ + guac_layer* select_layer; + + /** + * Whether text is currently selected. + */ + bool text_selected; + + /** + * The row that the selection starts at. + */ + int selection_start_row; + + /** + * The column that the selection starts at. + */ + int selection_start_column; + + /** + * The row that the selection ends at. + */ + int selection_end_row; + + /** + * The column that the selection ends at. + */ + int selection_end_column; } guac_terminal_display; /** - * Allocates a new display having the given text rendering properties and - * underlying graphical display. - * - * @param client - * The guac_client associated with the terminal session. - * - * @param graphical_display - * The guac_display that the new guac_terminal_display should render to. - * - * @param font_name - * The name of the font to use to render characters. - * - * @param font_size - * The font size to use when rendering characters, in points. - * - * @param dpi - * The resolution that characters should be rendered at, in DPI (dots per - * inch). - * - * @param foreground - * The default foreground color to use for characters rendered to the - * display. - * - * @param background - * The default background color to use for characters rendered to the - * display. - * - * @param palette - * The palette to use for all other colors supported by the terminal. + * Allocates a new display having the given default foreground and background + * colors. */ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, - guac_display* graphical_display, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, guac_terminal_color (*palette)[256]); @@ -234,11 +290,71 @@ int guac_terminal_display_assign_color(guac_terminal_display* display, int guac_terminal_display_lookup_color(guac_terminal_display* display, int index, guac_terminal_color* color); +/** + * Copies the given range of columns to a new location, offset from + * the original by the given number of columns. + */ +void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, + int start_column, int end_column, int offset); + +/** + * Copies the given range of rows to a new location, offset from the + * original by the given number of rows. + */ +void guac_terminal_display_copy_rows(guac_terminal_display* display, + int start_row, int end_row, int offset); + +/** + * Sets the given range of columns within the given row to the given + * character. + */ +void guac_terminal_display_set_columns(guac_terminal_display* display, int row, + int start_column, int end_column, guac_terminal_char* character); + /** * Resize the terminal to the given dimensions. */ void guac_terminal_display_resize(guac_terminal_display* display, int width, int height); +/** + * Flushes all pending operations within the given guac_terminal_display. + */ +void guac_terminal_display_flush(guac_terminal_display* display); + +/** + * Initializes and syncs the current terminal display state for all joining + * users associated with the provided socket, sending the necessary instructions + * to completely recreate and redraw the terminal rendering over the given + * socket. + * + * @param display + * The terminal display to sync to the users associated with the provided + * socket. + * + * @param client + * The client whose users are joining. + * + * @param socket + * The socket over which any necessary instructions should be sent. + */ +void guac_terminal_display_dup( + guac_terminal_display* display, guac_client* client, guac_socket* socket); + +/** + * Draws the text selection rectangle from the given coordinates to the given end coordinates. + */ +void guac_terminal_display_select(guac_terminal_display* display, + int start_row, int start_col, int end_row, int end_col); + +/** + * Clears the currently-selected region, removing the highlight. + * + * @param display + * The guac_terminal_display whose currently-selected region should be + * cleared. + */ +void guac_terminal_display_clear_select(guac_terminal_display* display); + /** * Alters the font of the terminal display. The available display area and the * regular grid of character cells will be resized as necessary to compensate @@ -269,80 +385,5 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int int guac_terminal_display_set_font(guac_terminal_display* display, const char* font_name, int font_size, int dpi); -/** - * Renders the contents of the given buffer to the given terminal display. All - * characters within the buffer that fit within the display region will be - * rendered. - * - * @param display - * The terminal display receiving the buffer contents. - * - * @param buffer - * The buffer to render to the display. - * - * @param scroll_offset - * The number of rows from the scrollback buffer that the user has scrolled - * into view. - * - * @param default_char - * The character that should be used to populate any character cell that - * has not received any terminal output. - * - * @param cursor_visible - * Whether the cursor is currently visible (ie: has not been hidden using - * console codes that specifically hide the cursor). This does NOT refer to - * whether the cursor is within the display region, which is handled - * automatically. - * - * @oaran cursor_row - * The current row position of the cursor. - * - * @oaran cursor_col - * The current column position of the cursor. - * - * @param text_selected - * Whether the user has selected text. - * - * @param selection_start_row - * The row number where the user started their selection. This value only - * has meaning if text_selected is true. There is no requirement that the - * start row be less than the end row. - * - * @param selection_start_col - * The column number where the user started their selection. This value - * only has meaning if text_selected is true. There is no requirement that - * the start column be less than the end column. - * - * @param selection_end_row - * The row number where the user ended their selection. This value only has - * meaning if text_selected is true. There is no requirement that the end - * row be greated than the start row. - * - * @param selection_end_col - * The column number where the user ended their selection. This value only - * has meaning if text_selected is true. There is no requirement that the - * end column be greater than the start column. - */ -void guac_terminal_display_render_buffer(guac_terminal_display* display, - guac_terminal_buffer* buffer, int scroll_offset, - guac_terminal_char* default_char, - bool cursor_visible, int cursor_row, int cursor_col, - bool text_selected, int selection_start_row, int selection_start_col, - int selection_end_row, int selection_end_col); - -/** - * Set the mouse cursor icon. If different from the mouse cursor in effect at - * the time of the previous guac_display frame, the requested cursor will take - * effect the next time the terminal display is flushed. - * - * @param display - * The display to set the cursor of. - * - * @param cursor - * The cursor to assign. - */ -void guac_terminal_display_set_cursor(guac_terminal_display* display, - guac_terminal_cursor_type cursor); - #endif diff --git a/src/terminal/terminal/scrollbar.h b/src/terminal/terminal/scrollbar.h index 60fbe3780..d29e459c3 100644 --- a/src/terminal/terminal/scrollbar.h +++ b/src/terminal/terminal/scrollbar.h @@ -27,7 +27,7 @@ */ #include -#include +#include /** * The width of the scrollbar, in pixels. @@ -97,11 +97,6 @@ typedef struct guac_terminal_scrollbar_render_state { } guac_terminal_scrollbar_render_state; -/** - * A scrollbar, made up of a containing layer and inner draggable handle. The - * position of the handle within the layer represents the value of the - * scrollbar. - */ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; /** @@ -111,6 +106,11 @@ typedef struct guac_terminal_scrollbar guac_terminal_scrollbar; typedef void guac_terminal_scrollbar_scroll_handler( guac_terminal_scrollbar* scrollbar, int value); +/** + * A scrollbar, made up of a containing layer and inner draggable handle. The + * position of the handle within the layer represents the value of the + * scrollbar. + */ struct guac_terminal_scrollbar { /** @@ -118,15 +118,10 @@ struct guac_terminal_scrollbar { */ guac_client* client; - /** - * The Guacamole display that this scrollbar should render to. - */ - guac_display* graphical_display; - /** * The layer containing the scrollbar. */ - guac_display_layer* parent; + const guac_layer* parent; /** * The width of the parent layer, in pixels. @@ -141,13 +136,13 @@ struct guac_terminal_scrollbar { /** * The scrollbar itself. */ - guac_display_layer* container; + guac_layer* container; /** * The draggable handle within the scrollbar, representing the current * scroll value. */ - guac_display_layer* handle; + guac_layer* handle; /** * The minimum scroll value. @@ -235,8 +230,8 @@ struct guac_terminal_scrollbar { * A newly allocated scrollbar. */ guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client, - guac_display* graphical_display, guac_display_layer* parent, - int parent_width, int parent_height, int visible_area); + const guac_layer* parent, int parent_width, int parent_height, + int visible_area); /** * Frees the given scrollbar. @@ -258,6 +253,24 @@ void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar); */ void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar); +/** + * Forces a complete redraw / resync of scrollbar state for all joining users + * associated with the provided socket, sending the necessary instructions to + * completely recreate and redraw the scrollbar rendering over the given + * socket. + * + * @param scrollbar + * The scrollbar to sync to the given users. + * + * @param client + * The client associated with the joining users. + * + * @param socket + * The socket over which any necessary instructions should be sent. + */ +void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar, + guac_client* client, guac_socket* socket); + /** * Sets the minimum and maximum allowed scroll values of the given scrollbar * to the given values. If necessary, the current value of the scrollbar will diff --git a/src/terminal/terminal/select.h b/src/terminal/terminal/select.h index 29e9d8b8f..8a2d96503 100644 --- a/src/terminal/terminal/select.h +++ b/src/terminal/terminal/select.h @@ -30,6 +30,18 @@ #include +/** + * Forwards the visible portion of the text selection rectangle to the + * underlying terminal display, requesting that it be redrawn. If no + * visible change would result from redrawing the selection rectangle, + * this function may have no effect. + * + * @param terminal + * The guac_terminal whose text selection rectangle should be + * redrawn. + */ +void guac_terminal_select_redraw(guac_terminal* terminal); + /** * Marks the start of text selection at the given row and column. Any existing * selection is cleared. This function should only be invoked while the diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 72e995de8..19d7d8da3 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -21,14 +21,13 @@ #define GUAC_TERMINAL_PRIV_H #include "common/clipboard.h" +#include "common/cursor.h" #include "buffer.h" #include "display.h" #include "scrollbar.h" #include "terminal.h" #include "typescript.h" -#include -#include #include /** @@ -60,11 +59,6 @@ struct guac_terminal { */ guac_client* client; - /** - * The Guacamole display that this terminal emulator should render to. - */ - guac_display* graphical_display; - /** * Whether user input should be handled and this terminal should render * frames. Initially, this will be false, user input will be ignored, and @@ -103,7 +97,7 @@ struct guac_terminal { * will require a frame flush. * * @see GUAC_TERMINAL_MODIFIED - */ + */ guac_flag modified; /** @@ -157,6 +151,11 @@ struct guac_terminal { */ guac_terminal_typescript* typescript; + /** + * Terminal-wide mouse cursor, synchronized across all users. + */ + guac_common_cursor* cursor; + /** * Graphical representation of the current scroll state. */ @@ -254,6 +253,18 @@ struct guac_terminal { */ bool cursor_visible; + /** + * The row of the rendered cursor. + * Will be set to -1 if the cursor is not visible. + */ + int visible_cursor_row; + + /** + * The column of the rendered cursor. + * Will be set to -1 if the cursor is not visible. + */ + int visible_cursor_col; + /** * The row of the saved cursor (ESC 7). */ @@ -364,6 +375,11 @@ struct guac_terminal { */ int selection_start_column; + /** + * The width of the character at selection start. + */ + int selection_start_width; + /** * The row that the selection ends at. */ @@ -374,6 +390,11 @@ struct guac_terminal { */ int selection_end_column; + /** + * The width of the character at selection end. + */ + int selection_end_width; + /** * Whether the cursor (arrow) keys should send cursor sequences * or application sequences (DECCKM). @@ -415,6 +436,11 @@ struct guac_terminal { */ int mouse_mask; + /** + * The current mouse cursor, to avoid re-setting the cursor image. + */ + guac_terminal_cursor_type current_cursor; + /** * The current contents of the clipboard. This clipboard instance is * maintained externally (will not be freed when this terminal is freed) @@ -535,6 +561,12 @@ void guac_terminal_scroll_up(guac_terminal* term, void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount); +/** + * Commits the current cursor location, updating the visible cursor + * on the screen. + */ +void guac_terminal_commit_cursor(guac_terminal* term); + /** * Scroll down the display by the given amount, replacing the new space with * data from the buffer. If not enough data is available, the maximum diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index bfeb49e76..11c8fb5a3 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -78,13 +78,13 @@ /** * The maximum duration of a single frame, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_DURATION 33 +#define GUAC_TERMINAL_FRAME_DURATION 40 /** * The maximum amount of time to wait for more data before declaring a frame * complete, in milliseconds. */ -#define GUAC_TERMINAL_FRAME_TIMEOUT 0 +#define GUAC_TERMINAL_FRAME_TIMEOUT 10 /** * The maximum number of custom tab stops. diff --git a/src/terminal/terminal/types.h b/src/terminal/terminal/types.h index 460b4f640..8415737aa 100644 --- a/src/terminal/terminal/types.h +++ b/src/terminal/terminal/types.h @@ -62,6 +62,11 @@ typedef struct guac_terminal_attributes { */ bool half_bright : 1; + /** + * Whether the associated character is highlighted by the cursor. + */ + bool cursor : 1; + /** * Whether the character should be rendered with reversed colors * (background becomes foreground and vice-versa). From c6833a460cfcbb934312b28352f7d9f680dc67df Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Sep 2024 20:38:36 -0700 Subject: [PATCH 40/53] GUACAMOLE-377: Make vertical combination more likely by limiting combinations to aligned boundaries. --- src/libguac/display-plan-combine.c | 67 +++++++++++++++++------------- src/libguac/display-plan.h | 24 ++++------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/libguac/display-plan-combine.c b/src/libguac/display-plan-combine.c index 32542a1b4..2a04151dd 100644 --- a/src/libguac/display-plan-combine.c +++ b/src/libguac/display-plan-combine.c @@ -23,31 +23,38 @@ #include "guacamole/rect.h" /** - * Returns whether the given operation can be combined with others. Only - * operations of certain types can be combined, and only up to a certain size - * (to favor parallelism). + * 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. * - * @param op - * The operation to test. + * 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 operation can be combined with others, zero otherwise. + * 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_is_combinable(const guac_display_plan_operation* op) { - switch (op->type) { +static int guac_display_plan_rect_crosses_boundary(const guac_rect* rect) { - case GUAC_DISPLAY_PLAN_OPERATION_IMG: - return guac_rect_width(&op->dest) < GUAC_DISPLAY_MAX_COMBINED_WIDTH - && guac_rect_height(&op->dest) < GUAC_DISPLAY_MAX_COMBINED_HEIGHT; + /* 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 */ - case GUAC_DISPLAY_PLAN_OPERATION_RECT: - case GUAC_DISPLAY_PLAN_OPERATION_COPY: - return 1; + guac_rect rect_copy = *rect; + guac_rect_align(&rect_copy, GUAC_DISPLAY_MAX_COMBINED_SIZE); - default: - return 0; + 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; - } } /** @@ -109,16 +116,14 @@ static int guac_display_plan_has_common_edge(const guac_display_plan_operation* static int guac_display_plan_should_combine(const guac_display_plan_operation* op_a, const guac_display_plan_operation* op_b) { - /* Consider only operations that have combinable types (draw to a - * particular rectangle in the layer) */ - if (!guac_display_plan_is_combinable(op_a) - || !guac_display_plan_is_combinable(op_b)) - return 0; - /* 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) { @@ -137,7 +142,8 @@ static int guac_display_plan_should_combine(const guac_display_plan_operation* o int delta_yb = op_b->dest.top - op_b->src.layer_rect.rect.top; return delta_xa == delta_xb - && delta_ya == delta_yb; + && delta_ya == delta_yb + && !guac_display_plan_rect_crosses_boundary(&combined); } break; @@ -147,7 +153,14 @@ static int guac_display_plan_should_combine(const guac_display_plan_operation* o * 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_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: @@ -156,10 +169,6 @@ static int guac_display_plan_should_combine(const guac_display_plan_operation* o } } - /* Simulate combination */ - guac_rect combined = op_a->dest; - guac_rect_extend(&combined, &op_b->dest); - /* Combine if result is still small */ int combined_width = guac_rect_width(&combined); int combined_height = guac_rect_height(&combined); diff --git a/src/libguac/display-plan.h b/src/libguac/display-plan.h index 76fecd0e2..d1a520b7f 100644 --- a/src/libguac/display-plan.h +++ b/src/libguac/display-plan.h @@ -47,22 +47,16 @@ #define GUAC_DISPLAY_DATA_FACTOR 128 /** - * The maximum width to allow when combining any pair of rendering operations - * into a single operation, in pixels. 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. - */ -#define GUAC_DISPLAY_MAX_COMBINED_WIDTH 512 - -/** - * The maximum height to allow when combining any pair of rendering operations - * into a single operation, in pixels. 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 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_HEIGHT 512 +#define GUAC_DISPLAY_MAX_COMBINED_SIZE 9 /** * The base cost of every update. Each update should be considered to have From 540d20474b4f25e4e78d04eebfc42e8ec4d5b0ee Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 20 Sep 2024 22:44:50 -0700 Subject: [PATCH 41/53] GUACAMOLE-377: Read lock should NOT be acquired at same time as write lock. Acquiring the read lock first and then reentrantly acquiring the write lock may result in other existing readers getting promoted to writers. --- src/libguac/display-flush.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c index 9fd76dfa6..44436168a 100644 --- a/src/libguac/display-flush.c +++ b/src/libguac/display-flush.c @@ -282,7 +282,7 @@ static int PFW_LFW_guac_display_frame_complete(guac_display* display) { void guac_display_end_mouse_frame(guac_display* display) { - guac_rwlock_acquire_read_lock(&display->pending_frame.lock); + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); if (!display->pending_frame_dirty_excluding_mouse) guac_display_end_multiple_frames(display, 0); From f34850665647abfe52c552e64f4f92baa9a1d297 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 23 Sep 2024 17:05:30 -0700 Subject: [PATCH 42/53] GUACAMOLE-377: Do NOT combine display operations vertically unless edges match exactly. Doing otherwise can result in display operations overlapping. This is because combining two vertically adjacent operations that have different widths causes additional cells to be included that are not covered by either original operation. If other operations are within those additional cells, then the operations flushed for the frame will overlap, consuming unnecessary additional processing and bandwidth. --- src/libguac/display-plan-combine.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libguac/display-plan-combine.c b/src/libguac/display-plan-combine.c index 2a04151dd..5e663c206 100644 --- a/src/libguac/display-plan-combine.c +++ b/src/libguac/display-plan-combine.c @@ -311,6 +311,7 @@ void PFW_guac_display_plan_combine_vertically(guac_display_plan* plan) { /* 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; } From 5ec7be130c48d2187690dc0e037ce1820cda56cf Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 25 Sep 2024 11:20:35 -0700 Subject: [PATCH 43/53] GUACAMOLE-377: Do not allow "img" instructions to break "rect" and "cfill" pairs. Other instructions that occur between "rect" and "cfill" may disrupt the path set by "rect", resulting in "cfill" having no effect, resulting in graphical artifacts. This also has the side effect of reducing thread contention by flushing the simple operations early (it is now less likely that multiple worker threads will be free for further tasks at nearly exactly the same time). --- src/libguac/display-plan.c | 60 +++++++++++++++++++++++++++++++++++- src/libguac/display-priv.h | 10 ------ src/libguac/display-worker.c | 38 ++++------------------- src/libguac/display.c | 4 --- 4 files changed, 65 insertions(+), 47 deletions(-) diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c index ec1bbead9..e92426ae9 100644 --- a/src/libguac/display-plan.c +++ b/src/libguac/display-plan.c @@ -20,9 +20,12 @@ #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 @@ -374,10 +377,65 @@ void guac_display_plan_free(guac_display_plan* 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_fifo_enqueue(&display->ops, op++); + + 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); + + 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-priv.h b/src/libguac/display-priv.h index ca9298f31..673bc456a 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -92,7 +92,6 @@ * 2) last_frame.lock * 3) ops * 4) render_state - * 5) op_path_lock * * Acquiring these locks in any other order risks deadlock. Don't do it. */ @@ -667,15 +666,6 @@ struct guac_display { */ guac_fifo ops; - /** - * Lock which ensures instructions that make use of a layer's current path - * (such as "rect" and "cfill") do not get inadvertently interleaved. - * Interleaving of path instructions can result in those paths not matching - * the expectatiosn of subsequent instructions, causing graphical - * artifacts. - */ - pthread_mutex_t op_path_lock; - /** * Storage for any items within the ops fifo. */ diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index e39513bbc..39649f3f7 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -104,16 +104,12 @@ static void guac_display_layer_clear_non_opaque(guac_display_layer* display_laye * being non-opaque */ if (!display_layer->opaque) { - pthread_mutex_lock(&display->op_path_lock); - guac_protocol_send_rect(socket, layer, dirty->left, dirty->top, guac_rect_width(dirty), guac_rect_height(dirty)); guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, 0x00, 0x00, 0x00, 0xFF); - pthread_mutex_unlock(&display->op_path_lock); - } } @@ -371,36 +367,14 @@ void* guac_display_worker_thread(void* data) { break; 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: - - pthread_mutex_lock(&display->op_path_lock); - 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); - - pthread_mutex_unlock(&display->op_path_lock); - break; - 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: diff --git a/src/libguac/display.c b/src/libguac/display.c index 731a44632..5cd739d65 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -137,9 +137,6 @@ guac_display* guac_display_alloc(guac_client* client) { guac_flag_init(&display->render_state); guac_flag_set(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); - /* Init lock specific to the GUAC_DISPLAY_PLAN_OPERATION_RECT operation */ - pthread_mutex_init(&display->op_path_lock, NULL); - int cpu_count = guac_display_nproc(); if (cpu_count <= 0) { guac_client_log(client, GUAC_LOG_WARNING, "Number of available " @@ -176,7 +173,6 @@ void guac_display_free(guac_display* display) { pthread_join(display->worker_threads[i], NULL); /* All locks, FIFOs, etc. are now unused and can be safely destroyed */ - pthread_mutex_destroy(&display->op_path_lock); guac_flag_destroy(&display->render_state); guac_fifo_destroy(&display->ops); guac_rwlock_destroy(&display->last_frame.lock); From 260f959b191c1f70ba61c5ac90936f15c2f3d11e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 25 Sep 2024 12:47:44 -0700 Subject: [PATCH 44/53] GUACAMOLE-377: Reduce number of worker threads to one per processor. Testing doesn't seem to support an increase in throughput or responsiveness from doubling up on worker threads. --- src/libguac/display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libguac/display.c b/src/libguac/display.c index 5cd739d65..0f1a3d996 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -44,7 +44,7 @@ /** * The number of worker threads to create per processor. */ -#define GUAC_DISPLAY_CPU_THREAD_FACTOR 2 +#define GUAC_DISPLAY_CPU_THREAD_FACTOR 1 /** * Returns the number of processors available to this process. If possible, From 8f5c3e9cc9832d5d4e34c0ddb3639a8160c04931 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Sep 2024 02:23:01 -0700 Subject: [PATCH 45/53] GUACAMOLE-377: Synchronize layer contents only if non-empty. --- src/libguac/display.c | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libguac/display.c b/src/libguac/display.c index 0f1a3d996..3e080e36b 100644 --- a/src/libguac/display.c +++ b/src/libguac/display.c @@ -218,21 +218,27 @@ void guac_display_dup(guac_display* display, guac_socket* socket) { int width = guac_rect_width(&layer_bounds); int height = guac_rect_height(&layer_bounds); + guac_protocol_send_size(socket, layer, width, height); - /* 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); + if (width > 0 && height > 0) { - /* Send PNG for rect */ - guac_protocol_send_size(socket, layer, width, height); - guac_client_stream_png(client, socket, GUAC_COMP_OVER, layer, 0, 0, rect); + /* 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); + /* 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) { From f2f74c2d0368ceb8a21ea80549ceec6686c620b7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Sep 2024 02:25:02 -0700 Subject: [PATCH 46/53] GUACAMOLE-377: Ignore alpha for rectangle operations on opaque layers. --- src/libguac/display-plan.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c index e92426ae9..212d93461 100644 --- a/src/libguac/display-plan.c +++ b/src/libguac/display-plan.c @@ -412,12 +412,12 @@ void guac_display_plan_apply(guac_display_plan* plan) { /* 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); + 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; From 0da3354e821a1b16ab08828195bc6abcb78a36b5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 18:16:38 -0700 Subject: [PATCH 47/53] GUACAMOLE-377: Add convenient, default render thread implementation for guac_display. --- src/libguac/Makefile.am | 1 + src/libguac/display-priv.h | 50 ++++++++ src/libguac/display-render-thread.c | 173 ++++++++++++++++++++++++++ src/libguac/guacamole/display-types.h | 6 + src/libguac/guacamole/display.h | 65 ++++++++++ 5 files changed, 295 insertions(+) create mode 100644 src/libguac/display-render-thread.c diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 6aedb4042..daf148198 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -135,6 +135,7 @@ libguac_la_SOURCES = \ 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 \ diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h index 673bc456a..d66020fa7 100644 --- a/src/libguac/display-priv.h +++ b/src/libguac/display-priv.h @@ -219,6 +219,56 @@ */ #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 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/guacamole/display-types.h b/src/libguac/guacamole/display-types.h index 3ca462956..61bbb4061 100644 --- a/src/libguac/guacamole/display-types.h +++ b/src/libguac/guacamole/display-types.h @@ -38,6 +38,12 @@ */ 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 diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h index 333b504dd..84c4c9082 100644 --- a/src/libguac/guacamole/display.h +++ b/src/libguac/guacamole/display.h @@ -708,6 +708,71 @@ guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_lay */ 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); + /** * @} */ From 98fd5e69a9efe9ebad08f2dc64e4abe006ae1940 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 18:20:04 -0700 Subject: [PATCH 48/53] GUACAMOLE-377: Migrate RDP to default render thread. --- src/protocols/rdp/client.h | 29 +++++------------ src/protocols/rdp/gdi.c | 12 +++---- src/protocols/rdp/pointer.c | 6 ++-- src/protocols/rdp/rdp.c | 61 +++++++++--------------------------- src/protocols/rdp/rdp.h | 13 ++------ src/protocols/rdp/settings.c | 4 +++ 6 files changed, 35 insertions(+), 90 deletions(-) diff --git a/src/protocols/rdp/client.h b/src/protocols/rdp/client.h index 9b307878b..7955db364 100644 --- a/src/protocols/rdp/client.h +++ b/src/protocols/rdp/client.h @@ -23,29 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. This ensures we at least - * meet a reasonable minimum framerate in the case that the RDP server provides - * no frame boundaries and streams data continuously enough that frame - * boundaries are not discernable through timing. + * 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_MAX_FRAME_DURATION 33 - -/** - * The minimum duration of a frame in milliseconds. This ensures we don't start - * flushing a ton of tiny frames if an RDP server provides no frame boundaries - * and streams data inconsistently enough that timing would suggest frame - * boundaries in the middle of a frame. - */ -#define GUAC_RDP_MIN_FRAME_DURATION 10 - -/** - * 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/gdi.c b/src/protocols/rdp/gdi.c index 8a9869595..69373faf9 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -39,15 +39,9 @@ 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; - /* A new frame is beginning */ - if (starting) { - rdp_client->in_frame = 1; - return; - } - /* A new frame has been received from the RDP server and processed */ - rdp_client->in_frame = 0; - rdp_client->frames_received++; + if (!starting) + guac_display_render_thread_notify_frame(rdp_client->render_thread); } @@ -133,6 +127,8 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_rect_constrain(&dst_rect, ¤t_context->bounds); guac_rect_extend(¤t_context->dirty, &dst_rect); + guac_display_render_thread_notify_modified(rdp_client->render_thread); + paint_complete: /* There will be no further drawing operations */ diff --git a/src/protocols/rdp/pointer.c b/src/protocols/rdp/pointer.c index 1a4f3f807..7387b46b8 100644 --- a/src/protocols/rdp/pointer.c +++ b/src/protocols/rdp/pointer.c @@ -103,7 +103,7 @@ BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* poi guac_display_layer_close_raw(cursor_layer, dst_context); guac_display_layer_close_raw(src_layer, src_context); - guac_display_end_mouse_frame(rdp_client->display); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } @@ -128,7 +128,7 @@ BOOL guac_rdp_pointer_set_null(rdpContext* context) { /* Set cursor to empty/blank graphic */ guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_NONE); - guac_display_end_mouse_frame(rdp_client->display); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } @@ -141,6 +141,6 @@ BOOL guac_rdp_pointer_set_default(rdpContext* context) { /* Set cursor to embedded pointer */ guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); - guac_display_end_mouse_frame(rdp_client->display); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 74f2d266b..fc74e65d2 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -597,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)) { @@ -605,48 +607,16 @@ 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) { - - /* Read server messages until frame is built */ - guac_timestamp frame_start = guac_timestamp_current(); - int frames_at_start = rdp_client->frames_received; - do { - - /* Handle any queued FreeRDP events (this may result in RDP - * messages being sent), aborting if FreeRDP event handling - * fails */ - if (!guac_rdp_handle_events(rdp_client)) { - wait_result = -1; - break; - } - - int frame_duration = guac_timestamp_current() - frame_start; - - if (!rdp_client->in_frame) { - - /* Flush frame if at least one frame has been produced */ - if (rdp_client->frames_received > frames_at_start) - break; - - /* Continue processing messages for up to a reasonable - * minimum framerate without an explicit frame boundary - * indicating that the frame is not yet complete */ - if (frame_duration > GUAC_RDP_MAX_FRAME_DURATION) - break; - - } - /* Do not exceed a reasonable maximum framerate without an - * explicit frame boundary terminating the frame early */ - int allowed_wait = GUAC_RDP_MIN_FRAME_DURATION - frame_duration; - if (allowed_wait < 0) - allowed_wait = 0; - - wait_result = rdp_guac_client_wait_for_messages(client, allowed_wait); - - } 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 if FreeRDP event handling fails */ + if (!guac_rdp_handle_events(rdp_client)) { + wait_result = -1; + break; } /* Test whether the RDP server is closing the connection */ @@ -666,13 +636,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->in_frame) { - guac_display_end_multiple_frames(rdp_client->display, rdp_client->frames_received); - rdp_client->frames_received = 0; - } - } guac_rwlock_acquire_write_lock(&(rdp_client->lock)); @@ -688,6 +651,10 @@ static int guac_rdp_handle_connection(guac_client* client) { freerdp_disconnect(rdp_inst); pthread_mutex_unlock(&(rdp_client->message_lock)); + /* 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); diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 5d723c30a..76ca18ca3 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -115,17 +115,10 @@ typedef struct guac_rdp_client { 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 47320dfcc..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) { From 5a12d96ac593546e38b78bc6e0695c98f6774b8b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 18:20:27 -0700 Subject: [PATCH 49/53] GUACAMOLE-377: Migrate VNC to default render loop. --- src/protocols/vnc/client.h | 29 ++++++----------------- src/protocols/vnc/cursor.c | 1 + src/protocols/vnc/display.c | 2 ++ src/protocols/vnc/vnc.c | 46 +++++++++++-------------------------- src/protocols/vnc/vnc.h | 6 +++++ 5 files changed, 30 insertions(+), 54 deletions(-) diff --git a/src/protocols/vnc/client.h b/src/protocols/vnc/client.h index dd0ec7413..86fe264ad 100644 --- a/src/protocols/vnc/client.h +++ b/src/protocols/vnc/client.h @@ -23,29 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. This ensures we at least - * meet a reasonable minimum framerate in the case that the VNC server provides - * no frame boundaries and streams data continuously enough that frame - * boundaries are not discernable through timing. + * 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_MAX_FRAME_DURATION 33 - -/** - * The minimum duration of a frame in milliseconds. This ensures we don't start - * flushing a ton of tiny frames if an VNC server provides no frame boundaries - * and streams data inconsistently enough that timing would suggest frame - * boundaries in the middle of a frame. - */ -#define GUAC_VNC_MIN_FRAME_DURATION 10 - -/** - * The amount of time to wait for a new message from the VNC server when - * beginning a new frame, 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_START_TIMEOUT 1000 +#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 7a1bc1518..079ddd36f 100644 --- a/src/protocols/vnc/cursor.c +++ b/src/protocols/vnc/cursor.c @@ -114,6 +114,7 @@ void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int vnc_bpp) /* 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) { diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 70b03be8a..8b09f8b6e 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -129,6 +129,8 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { vnc_client->copy_rect_used = 0; } + guac_display_render_thread_notify_modified(vnc_client->render_thread); + } void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index cdd21bd63..f72ad7a96 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -596,44 +596,22 @@ void* guac_vnc_client_thread(void* data) { guac_display_end_frame(vnc_client->display); + 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 data and construct a reasonable frame */ - int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_FRAME_START_TIMEOUT); + int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_MESSAGE_CHECK_INTERVAL); if (wait_result > 0) { - /* Read server messages until frame is built */ - guac_timestamp frame_start = guac_timestamp_current(); - do { - - /* 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; - } - - int frame_duration = guac_timestamp_current() - frame_start; - - /* Continue processing messages for up to a reasonable minimum - * framerate without an explicit frame boundary indicating that - * the frame is not yet complete */ - if (frame_duration > GUAC_VNC_MAX_FRAME_DURATION) - break; - - /* Do not exceed a reasonable maximum framerate without an - * explicit frame boundary terminating the frame early */ - int allowed_wait = GUAC_VNC_MIN_FRAME_DURATION - frame_duration; - if (allowed_wait < 0) - allowed_wait = 0; - - wait_result = guac_vnc_wait_for_messages(rfb_client, allowed_wait); - - } while (wait_result > 0); - - guac_display_end_frame(vnc_client->display); + /* 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; + } } @@ -643,6 +621,10 @@ void* guac_vnc_client_thread(void* data) { } + /* 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 7e5676af8..6b83d41d0 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -113,6 +113,12 @@ typedef struct guac_vnc_client { */ 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. */ From db42844ec9c87cf9d65823231901f20521a1cc81 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 19:25:29 -0700 Subject: [PATCH 50/53] GUACAMOLE-377: Remove superfluous bounds check (now part of guac_pool). --- src/libguac/client.c | 6 +----- src/libguac/user.c | 12 ++---------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/libguac/client.c b/src/libguac/client.c index efe6d53ae..ab8a07c1d 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -127,11 +127,7 @@ 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) - return NULL; - - /* Allocate stream */ + /* Allocate stream, but refuse to allocate beyond maximum */ stream_index = guac_pool_next_int_below_or_die(client->__stream_pool, GUAC_CLIENT_MAX_STREAMS); diff --git a/src/libguac/user.c b/src/libguac/user.c index c2b320428..160e27478 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -107,11 +107,7 @@ 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) - return NULL; - - /* Allocate stream */ + /* Allocate stream, but refuse to allocate beyond maximum */ stream_index = guac_pool_next_int_below_or_die(user->__stream_pool, GUAC_USER_MAX_STREAMS); @@ -142,11 +138,7 @@ 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) - return NULL; - - /* Allocate object */ + /* Allocate object, but refuse to allocate beyond maximum */ object_index = guac_pool_next_int_below_or_die(user->__object_pool, GUAC_USER_MAX_OBJECTS); From ed388631b5e11ea912dee6e4e24e088694218a15 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 19:29:46 -0700 Subject: [PATCH 51/53] GUACAMOLE-377: Restore original NULL returns for alloc failures of streams/objects. --- src/libguac/client.c | 5 +++-- src/libguac/user.c | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libguac/client.c b/src/libguac/client.c index ab8a07c1d..71fa7f763 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -128,8 +128,9 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { int stream_index; /* Allocate stream, but refuse to allocate beyond maximum */ - stream_index = guac_pool_next_int_below_or_die(client->__stream_pool, - GUAC_CLIENT_MAX_STREAMS); + stream_index = guac_pool_next_int_below(client->__stream_pool, GUAC_CLIENT_MAX_STREAMS); + if (stream_index < 0) + return NULL; /* Initialize stream with odd index (even indices are user-level) */ allocd_stream = &(client->__output_streams[stream_index]); diff --git a/src/libguac/user.c b/src/libguac/user.c index 160e27478..16baa169f 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -108,8 +108,9 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { int stream_index; /* Allocate stream, but refuse to allocate beyond maximum */ - stream_index = guac_pool_next_int_below_or_die(user->__stream_pool, - GUAC_USER_MAX_STREAMS); + stream_index = guac_pool_next_int_below(user->__stream_pool, GUAC_USER_MAX_STREAMS); + if (stream_index < 0) + return NULL; /* Initialize stream with even index (odd indices are client-level) */ allocd_stream = &(user->__output_streams[stream_index]); @@ -139,8 +140,9 @@ guac_object* guac_user_alloc_object(guac_user* user) { int object_index; /* Allocate object, but refuse to allocate beyond maximum */ - object_index = guac_pool_next_int_below_or_die(user->__object_pool, - GUAC_USER_MAX_OBJECTS); + object_index = guac_pool_next_int_below(user->__object_pool, GUAC_USER_MAX_OBJECTS); + if (object_index < 0) + return NULL; /* Initialize object */ allocd_object = &(user->__objects[object_index]); From 4220670dacd254e1b6b6bd8ccbc22637b5d9f1cd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Sep 2024 20:23:04 -0700 Subject: [PATCH 52/53] GUACAMOLE-377: Fix regression causing RDP connections to always reconnect after closing. --- src/protocols/rdp/rdp.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index fc74e65d2..a6e7720d3 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -613,11 +613,9 @@ static int guac_rdp_handle_connection(guac_client* client) { break; /* Handle any queued FreeRDP events (this may result in RDP messages - * being sent), aborting if FreeRDP event handling fails */ - if (!guac_rdp_handle_events(rdp_client)) { + * being sent), aborting later if FreeRDP event handling fails */ + if (!guac_rdp_handle_events(rdp_client)) wait_result = -1; - break; - } /* Test whether the RDP server is closing the connection */ int connection_closing; From f5ecb6c8f9731a1ad8bf7c95f3df93915b3f8372 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 28 Sep 2024 10:09:05 -0700 Subject: [PATCH 53/53] GUACAMOLE-377: Stream/object sentinel must be assigned BEFORE returning index to pool. Doing otherwise results in a race condition where the index of a valid stream is changed to -1 by a different thread, breaking assertions and causing the connection to disconnect. --- src/libguac/client.c | 7 ++++--- src/libguac/user.c | 14 ++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libguac/client.c b/src/libguac/client.c index 71fa7f763..5926e9ca6 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -146,12 +146,13 @@ 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); + } /** diff --git a/src/libguac/user.c b/src/libguac/user.c index 16baa169f..753aca3fa 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -126,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) { @@ -157,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) {