Merge 1.5.2 changes back to master.
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
new file mode 100644
index 0000000..1aa6407
--- /dev/null
+++ b/.github/workflows/pr-build.yml
@@ -0,0 +1,29 @@
+name: Pull request CI build
+
+# Run build for all pull requests
+on:
+  pull_request:
+
+# Limit to only one build for a given PR source branch at a time,
+# cancelling any in-progress builds
+concurrency:
+  group: guacamole-server-pr-${{ github.head_ref }}
+  cancel-in-progress: true
+
+jobs:
+
+  docker_build:
+    name: Run docker build
+    runs-on: ubuntu-latest
+    steps:
+
+      - name: Check out code
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Build Docker container
+        shell: sh
+        run: |
+          docker build --pull --no-cache --force-rm .
diff --git a/src/common-ssh/ssh.c b/src/common-ssh/ssh.c
index 373b9f1..1cbd483 100644
--- a/src/common-ssh/ssh.c
+++ b/src/common-ssh/ssh.c
@@ -180,9 +180,11 @@
     CRYPTO_set_locking_callback(guac_common_ssh_openssl_locking_callback);
 #endif
 
-    /* Init OpenSSL */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+    /* Init OpenSSL - only required for OpenSSL Versions < 1.1.0 */
     SSL_library_init();
     ERR_load_crypto_strings();
+#endif
 
     /* Init libssh2 */
     libssh2_init(0);
diff --git a/src/common/display.c b/src/common/display.c
index 9b4b87f..e8465e4 100644
--- a/src/common/display.c
+++ b/src/common/display.c
@@ -166,6 +166,8 @@
 void guac_common_display_dup(guac_common_display* display, guac_user* user,
         guac_socket* socket) {
 
+    guac_client* client = user->client;
+
     pthread_mutex_lock(&display->_lock);
 
     /* Sunchronize shared cursor */
@@ -178,6 +180,9 @@
     guac_common_display_dup_layers(display->layers, user, socket);
     guac_common_display_dup_layers(display->buffers, user, 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);
 
 }
@@ -384,4 +389,3 @@
     pthread_mutex_unlock(&display->_lock);
 
 }
-
diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c
index 2861cff..8bf3035 100644
--- a/src/guacd/daemon.c
+++ b/src/guacd/daemon.c
@@ -381,10 +381,15 @@
         CRYPTO_set_locking_callback(guacd_openssl_locking_callback);
 #endif
 
-        /* Init SSL */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+        /* Init OpenSSL for OpenSSL Versions < 1.1.0 */
         SSL_library_init();
         SSL_load_error_strings();
         ssl_context = SSL_CTX_new(SSLv23_server_method());
+#else
+        /* Set up OpenSSL for OpenSSL Versions >= 1.1.0 */
+        ssl_context = SSL_CTX_new(TLS_server_method());
+#endif
 
         /* Load key */
         if (config->key_file != NULL) {
diff --git a/src/guacenc/ffmpeg-compat.c b/src/guacenc/ffmpeg-compat.c
index 54e4b74..2412e48 100644
--- a/src/guacenc/ffmpeg-compat.c
+++ b/src/guacenc/ffmpeg-compat.c
@@ -213,7 +213,7 @@
 #endif
 }
 
-AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, AVCodec* codec, 
+AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, const AVCodec* codec,
         int bitrate, int width, int height, int gop_size, int qmax, int qmin,
         int pix_fmt, AVRational time_base) {
 
@@ -249,7 +249,7 @@
 }
 
 int guacenc_open_avcodec(AVCodecContext *avcodec_context,
-        AVCodec *codec, AVDictionary **options,
+        const AVCodec *codec, AVDictionary **options,
         AVStream* stream) {
 
     int ret = avcodec_open2(avcodec_context, codec, options);
diff --git a/src/guacenc/ffmpeg-compat.h b/src/guacenc/ffmpeg-compat.h
index a5cfb2f..d75b5d7 100644
--- a/src/guacenc/ffmpeg-compat.h
+++ b/src/guacenc/ffmpeg-compat.h
@@ -128,7 +128,7 @@
  *     The pointer to the configured AVCodecContext.
  *
  */
-AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, AVCodec* codec,
+AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, const AVCodec* codec,
         int bitrate, int width, int height, int gop_size, int qmax, int qmin,
         int pix_fmt, AVRational time_base);
 
@@ -158,7 +158,7 @@
  *     Zero on success, a negative value on error.
  */
 int guacenc_open_avcodec(AVCodecContext *avcodec_context,
-        AVCodec *codec, AVDictionary **options,
+        const AVCodec *codec, AVDictionary **options,
         AVStream* stream);
 
 #endif
diff --git a/src/guacenc/video.c b/src/guacenc/video.c
index e2eed51..c8fb8b9 100644
--- a/src/guacenc/video.c
+++ b/src/guacenc/video.c
@@ -47,7 +47,7 @@
 guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
         int width, int height, int bitrate) {
 
-    AVOutputFormat *container_format;
+    const AVOutputFormat *container_format;
     AVFormatContext *container_format_context;
     AVStream *video_stream;
     int ret;
@@ -63,7 +63,7 @@
     container_format = container_format_context->oformat;
 
     /* Pull codec based on name */
-    AVCodec* codec = avcodec_find_encoder_by_name(codec_name);
+    const AVCodec* codec = avcodec_find_encoder_by_name(codec_name);
     if (codec == NULL) {
         guacenc_log(GUAC_LOG_ERROR, "Failed to locate codec \"%s\".",
                 codec_name);
diff --git a/src/libguac/client.c b/src/libguac/client.c
index 952ffcc..46291b4 100644
--- a/src/libguac/client.c
+++ b/src/libguac/client.c
@@ -421,15 +421,19 @@
 }
 
 int guac_client_end_frame(guac_client* client) {
+    return guac_client_end_multiple_frames(client, 0);
+}
+
+int guac_client_end_multiple_frames(guac_client* client, int frames) {
 
     /* Update and send timestamp */
     client->last_sent_timestamp = guac_timestamp_current();
 
     /* Log received timestamp and calculated lag (at TRACE level only) */
     guac_client_log(client, GUAC_LOG_TRACE, "Server completed "
-            "frame %" PRIu64 "ms.", client->last_sent_timestamp);
+            "frame %" PRIu64 "ms (%i logical frames)", client->last_sent_timestamp, frames);
 
-    return guac_protocol_send_sync(client->socket, client->last_sent_timestamp);
+    return guac_protocol_send_sync(client->socket, client->last_sent_timestamp, frames);
 
 }
 
diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h
index 5712476..a64dee6 100644
--- a/src/libguac/guacamole/client.h
+++ b/src/libguac/guacamole/client.h
@@ -509,19 +509,48 @@
         guac_user_callback* callback, void* data);
 
 /**
- * Marks the end of the current frame by sending a "sync" instruction to
- * all connected users. This instruction will contain the current timestamp.
- * The last_sent_timestamp member of guac_client will be updated accordingly.
+ * Marks the end of the current frame by sending a "sync" instruction to all
+ * connected users, where the number of input frames that were considered in
+ * creating this frame is either unknown or inapplicable. This instruction will
+ * contain the current timestamp. The last_sent_timestamp member of guac_client
+ * will be updated accordingly.
  *
  * If an error occurs sending the instruction, a non-zero value is
  * returned, and guac_error is set appropriately.
  *
- * @param client The guac_client which has finished a frame.
- * @return Zero on success, non-zero on error.
+ * @param client
+ *     The guac_client which has finished a frame.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
  */
 int guac_client_end_frame(guac_client* client);
 
 /**
+ * Marks the end of the current frame by sending a "sync" instruction to all
+ * connected users, where that frame may combine or otherwise represent the
+ * changes of an arbitrary number of input frames. This instruction will
+ * contain the current timestamp, as well as the number of frames that were
+ * considered in creating that frame.  The last_sent_timestamp member of
+ * guac_client will be updated accordingly.
+ *
+ * If an error occurs sending the instruction, a non-zero value is
+ * returned, and guac_error is set appropriately.
+ *
+ * @param client
+ *     The guac_client which has finished a frame.
+ *
+ * @param frames
+ *     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.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
+ */
+int guac_client_end_multiple_frames(guac_client* client, int frames);
+
+/**
  * Initializes the given guac_client using the initialization routine provided
  * by the plugin corresponding to the named protocol. This will automatically
  * invoke guac_client_init within the plugin for the given protocol.
diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h
index 91debdd..1eb4fd6 100644
--- a/src/libguac/guacamole/protocol.h
+++ b/src/libguac/guacamole/protocol.h
@@ -384,11 +384,22 @@
  * If an error occurs sending the instruction, a non-zero value is
  * returned, and guac_error is set appropriately.
  *
- * @param socket The guac_socket connection to use.
- * @param timestamp The current timestamp (in milliseconds).
- * @return Zero on success, non-zero on error.
+ * @param socket
+ *     The guac_socket connection to use.
+ *
+ * @param timestamp
+ *     The current timestamp (in milliseconds).
+ *
+ * @param frames
+ *     The number of distinct frames that were considered or combined when
+ *     generating the frame terminated by this instruction, or zero if the
+ *     boundaries of relevant frames are unknown.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
  */
-int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp);
+int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp,
+        int frames);
 
 /* OBJECT INSTRUCTIONS */
 
diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c
index 3421af5..b415dac 100644
--- a/src/libguac/protocol.c
+++ b/src/libguac/protocol.c
@@ -1199,7 +1199,8 @@
 
 }
 
-int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp) {
+int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp,
+        int frames) {
 
     int ret_val;
 
@@ -1207,6 +1208,8 @@
     ret_val = 
            guac_socket_write_string(socket, "4.sync,")
         || __guac_socket_write_length_int(socket, timestamp)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, frames)
         || guac_socket_write_string(socket, ";");
 
     guac_socket_instruction_end(socket);
diff --git a/src/libguac/tests/socket/fd_send_instruction.c b/src/libguac/tests/socket/fd_send_instruction.c
index 7d991e6..abbf5cc 100644
--- a/src/libguac/tests/socket/fd_send_instruction.c
+++ b/src/libguac/tests/socket/fd_send_instruction.c
@@ -54,7 +54,7 @@
 
     /* Write instructions */
     guac_protocol_send_name(socket, "a" UTF8_4 "b" UTF8_4 "c");
-    guac_protocol_send_sync(socket, 12345);
+    guac_protocol_send_sync(socket, 12345, 1);
     guac_socket_flush(socket);
 
     /* Close and free socket */
@@ -76,7 +76,7 @@
 
     char expected[] =
         "4.name,11.a" UTF8_4 "b" UTF8_4 "c;"
-        "4.sync,5.12345;";
+        "4.sync,5.12345,1.1;";
 
     int numread;
     char buffer[1024];
diff --git a/src/libguac/tests/socket/nested_send_instruction.c b/src/libguac/tests/socket/nested_send_instruction.c
index c6d3375..14e876b 100644
--- a/src/libguac/tests/socket/nested_send_instruction.c
+++ b/src/libguac/tests/socket/nested_send_instruction.c
@@ -65,7 +65,7 @@
 
     /* Write instructions */
     guac_protocol_send_name(nested_socket, "a" UTF8_4 "b" UTF8_4 "c");
-    guac_protocol_send_sync(nested_socket, 12345);
+    guac_protocol_send_sync(nested_socket, 12345, 1);
 
     /* Close and free sockets */
     guac_socket_free(nested_socket);
@@ -86,9 +86,9 @@
 static void read_expected_instructions(int fd) {
 
     char expected[] =
-        "4.nest,3.123,37."
+        "4.nest,3.123,41."
             "4.name,11.a" UTF8_4 "b" UTF8_4 "c;"
-            "4.sync,5.12345;"
+            "4.sync,5.12345,1.1;"
         ";";
 
     int numread;
diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c
index db1e7b8..be7f1a1 100644
--- a/src/libguac/user-handlers.c
+++ b/src/libguac/user-handlers.c
@@ -121,31 +121,39 @@
         /* Calculate length of frame, including network and processing lag */
         frame_duration = current - timestamp;
 
-        /* Update lag statistics if at least one frame has been rendered */
+        /* Calculate processing lag portion of length of frame */
+        int frame_processing_lag = 0;
         if (user->last_frame_duration != 0) {
 
             /* Calculate lag using the previous frame as a baseline */
-            int processing_lag = frame_duration - user->last_frame_duration;
+            frame_processing_lag = frame_duration - user->last_frame_duration;
 
             /* Adjust back to zero if cumulative error leads to a negative
              * value */
-            if (processing_lag < 0)
-                processing_lag = 0;
-
-            user->processing_lag = processing_lag;
+            if (frame_processing_lag < 0)
+                frame_processing_lag = 0;
 
         }
 
-        /* Record baseline duration of frame by excluding lag */
-        user->last_frame_duration = frame_duration - user->processing_lag;
+        /* Record baseline duration of frame by excluding lag (this is the
+         * network round-trip time) */
+        int estimated_rtt = frame_duration - frame_processing_lag;
+        user->last_frame_duration = estimated_rtt;
+
+        /* Calculate cumulative accumulated processing lag relative to server timeline */
+        int processing_lag = current - user->last_received_timestamp - estimated_rtt;
+        if (processing_lag < 0)
+            processing_lag = 0;
+
+        user->processing_lag = processing_lag;
 
     }
 
     /* Log received timestamp and calculated lag (at TRACE level only) */
     guac_user_log(user, GUAC_LOG_TRACE,
             "User confirmation of frame %" PRIu64 "ms received "
-            "at %" PRIu64 "ms (processing_lag=%ims)",
-            timestamp, current, user->processing_lag);
+            "at %" PRIu64 "ms (processing_lag=%ims, estimated_rtt=%ims)",
+            timestamp, current, user->processing_lag, user->last_frame_duration);
 
     if (user->sync_handler)
         return user->sync_handler(user, timestamp);
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index 89c094b..fe7a3fb 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -57,6 +57,7 @@
     channels/rdpdr/rdpdr-printer.c               \
     channels/rdpdr/rdpdr.c                       \
     channels/rdpei.c                             \
+    channels/rdpgfx.c                            \
     channels/rdpsnd/rdpsnd-messages.c            \
     channels/rdpsnd/rdpsnd.c                     \
     client.c                                     \
@@ -103,6 +104,7 @@
     channels/rdpdr/rdpdr-printer.h               \
     channels/rdpdr/rdpdr.h                       \
     channels/rdpei.h                             \
+    channels/rdpgfx.h                            \
     channels/rdpsnd/rdpsnd-messages.h            \
     channels/rdpsnd/rdpsnd.h                     \
     client.h                                     \
@@ -228,6 +230,7 @@
 rdp_keymaps =                                \
     $(srcdir)/keymaps/base.keymap            \
     $(srcdir)/keymaps/failsafe.keymap        \
+    $(srcdir)/keymaps/cs-cz-qwertz.keymap    \
     $(srcdir)/keymaps/de_de_qwertz.keymap    \
     $(srcdir)/keymaps/de_ch_qwertz.keymap    \
     $(srcdir)/keymaps/en_gb_qwerty.keymap    \
@@ -235,6 +238,7 @@
     $(srcdir)/keymaps/es_es_qwerty.keymap    \
     $(srcdir)/keymaps/es_latam_qwerty.keymap \
     $(srcdir)/keymaps/fr_be_azerty.keymap    \
+    $(srcdir)/keymaps/fr_ca_qwerty.keymap    \
     $(srcdir)/keymaps/fr_ch_qwertz.keymap    \
     $(srcdir)/keymaps/fr_fr_azerty.keymap    \
     $(srcdir)/keymaps/hu_hu_qwertz.keymap    \
@@ -243,6 +247,8 @@
     $(srcdir)/keymaps/no_no_qwerty.keymap    \
     $(srcdir)/keymaps/pl_pl_qwerty.keymap    \
     $(srcdir)/keymaps/pt_br_qwerty.keymap    \
+    $(srcdir)/keymaps/pt_pt_qwerty.keymap    \
+    $(srcdir)/keymaps/ro_ro_qwerty.keymap    \
     $(srcdir)/keymaps/sv_se_qwerty.keymap    \
     $(srcdir)/keymaps/da_dk_qwerty.keymap    \
     $(srcdir)/keymaps/tr_tr_qwerty.keymap
diff --git a/src/protocols/rdp/channels/cliprdr.c b/src/protocols/rdp/channels/cliprdr.c
index 4845577..f16b41c 100644
--- a/src/protocols/rdp/channels/cliprdr.c
+++ b/src/protocols/rdp/channels/cliprdr.c
@@ -509,12 +509,12 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_cliprdr_channel_connected(rdpContext* context,
-        ChannelConnectedEventArgs* e) {
+        ChannelConnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -526,12 +526,12 @@
     assert(clipboard != NULL);
 
     /* Ignore connection event if it's not for the CLIPRDR channel */
-    if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
         return;
 
     /* The structure pointed to by pInterface is guaranteed to be a
      * CliprdrClientContext if the channel is CLIPRDR */
-    CliprdrClientContext* cliprdr = (CliprdrClientContext*) e->pInterface;
+    CliprdrClientContext* cliprdr = (CliprdrClientContext*) args->pInterface;
 
     /* Associate FreeRDP CLIPRDR context and its Guacamole counterpart with
      * eachother */
@@ -562,12 +562,12 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_cliprdr_channel_disconnected(rdpContext* context,
-        ChannelDisconnectedEventArgs* e) {
+        ChannelDisconnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -579,7 +579,7 @@
     assert(clipboard != NULL);
 
     /* Ignore disconnection event if it's not for the CLIPRDR channel */
-    if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
         return;
 
     /* Channel is no longer connected */
diff --git a/src/protocols/rdp/channels/disp.c b/src/protocols/rdp/channels/disp.c
index 70342eb..918676b 100644
--- a/src/protocols/rdp/channels/disp.c
+++ b/src/protocols/rdp/channels/disp.c
@@ -68,19 +68,19 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_disp_channel_connected(rdpContext* context,
-        ChannelConnectedEventArgs* e) {
+        ChannelConnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_disp* guac_disp = rdp_client->disp;
 
     /* Ignore connection event if it's not for the Display Update channel */
-    if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, DISP_DVC_CHANNEL_NAME) != 0)
         return;
 
     /* Init module with current display size */
@@ -89,7 +89,7 @@
             guac_rdp_get_height(context->instance));
 
     /* Store reference to the display update plugin once it's connected */
-    DispClientContext* disp = (DispClientContext*) e->pInterface;
+    DispClientContext* disp = (DispClientContext*) args->pInterface;
     guac_disp->disp = disp;
 
     guac_client_log(client, GUAC_LOG_DEBUG, "Display update channel "
@@ -110,19 +110,19 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_disp_channel_disconnected(rdpContext* context,
-        ChannelDisconnectedEventArgs* e) {
+        ChannelDisconnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_disp* guac_disp = rdp_client->disp;
 
     /* Ignore disconnection event if it's not for the Display Update channel */
-    if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, DISP_DVC_CHANNEL_NAME) != 0)
         return;
 
     /* Channel is no longer connected */
diff --git a/src/protocols/rdp/channels/rail.c b/src/protocols/rdp/channels/rail.c
index 6f8d7f7..e8ac169 100644
--- a/src/protocols/rdp/channels/rail.c
+++ b/src/protocols/rdp/channels/rail.c
@@ -230,22 +230,22 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_rail_channel_connected(rdpContext* context,
-        ChannelConnectedEventArgs* e) {
+        ChannelConnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
 
     /* Ignore connection event if it's not for the RAIL channel */
-    if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, RAIL_SVC_CHANNEL_NAME) != 0)
         return;
 
     /* The structure pointed to by pInterface is guaranteed to be a
      * RailClientContext if the channel is RAIL */
-    RailClientContext* rail = (RailClientContext*) e->pInterface;
+    RailClientContext* rail = (RailClientContext*) args->pInterface;
 
     /* Init FreeRDP RAIL context, ensuring the guac_client can be accessed from
      * within any RAIL-specific callbacks */
diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c
index 06d2ba4..a7e6f34 100644
--- a/src/protocols/rdp/channels/rdpei.c
+++ b/src/protocols/rdp/channels/rdpei.c
@@ -66,23 +66,23 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_rdpei_channel_connected(rdpContext* context,
-        ChannelConnectedEventArgs* e) {
+        ChannelConnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;
 
     /* Ignore connection event if it's not for the RDPEI channel */
-    if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, RDPEI_DVC_CHANNEL_NAME) != 0)
         return;
 
     /* Store reference to the RDPEI plugin once it's connected */
-    RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface;
+    RdpeiClientContext* rdpei = (RdpeiClientContext*) args->pInterface;
     guac_rdpei->rdpei = rdpei;
 
     /* Declare level of multi-touch support */
@@ -107,19 +107,19 @@
  * @param context
  *     The rdpContext associated with the active RDP session.
  *
- * @param e
+ * @param args
  *     Event-specific arguments, mainly the name of the channel, and a
  *     reference to the associated plugin loaded for that channel by FreeRDP.
  */
 static void guac_rdp_rdpei_channel_disconnected(rdpContext* context,
-        ChannelDisconnectedEventArgs* e) {
+        ChannelDisconnectedEventArgs* args) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;
 
     /* Ignore disconnection event if it's not for the RDPEI channel */
-    if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
+    if (strcmp(args->name, RDPEI_DVC_CHANNEL_NAME) != 0)
         return;
 
     /* Channel is no longer connected */
diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c
new file mode 100644
index 0000000..0fae972
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpgfx.c
@@ -0,0 +1,122 @@
+/*
+ * 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 "channels/rdpgfx.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/client/rdpgfx.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/gdi/gfx.h>
+#include <freerdp/event.h>
+#include <guacamole/client.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * RdpgfxClientContext instance allocated by FreeRDP to deal with received
+ * RDPGFX (Graphics Pipeline) messages.
+ *
+ * This function is called whenever a channel connects via the PubSub event
+ * system within FreeRDP, but only has any effect if the connected channel is
+ * the RDPGFX channel. This specific callback is registered with the
+ * PubSub system of the relevant rdpContext when guac_rdp_rdpgfx_load_plugin() is
+ * called.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ *
+ * @param args
+ *     Event-specific arguments, mainly the name of the channel, and a
+ *     reference to the associated plugin loaded for that channel by FreeRDP.
+ */
+static void guac_rdp_rdpgfx_channel_connected(rdpContext* context,
+        ChannelConnectedEventArgs* args) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Ignore connection event if it's not for the RDPGFX channel */
+    if (strcmp(args->name, RDPGFX_DVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* Init GDI-backed support for the Graphics Pipeline */
+    RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface;
+    rdpGdi* gdi = context->gdi;
+
+    if (!gdi_graphics_pipeline_init(gdi, rdpgfx))
+        guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX "
+                "channel could not be loaded. Graphics may not render at all!");
+    else
+        guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel will be used for "
+                "the RDP Graphics Pipeline Extension.");
+
+}
+
+/**
+ * Callback which handles any RDPGFX cleanup specific to Guacamole.
+ *
+ * This function is called whenever a channel disconnects via the PubSub event
+ * system within FreeRDP, but only has any effect if the disconnected channel
+ * is the RDPGFX channel. This specific callback is registered with the PubSub
+ * system of the relevant rdpContext when guac_rdp_rdpgfx_load_plugin() is
+ * called.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ *
+ * @param args
+ *     Event-specific arguments, mainly the name of the channel, and a
+ *     reference to the associated plugin loaded for that channel by FreeRDP.
+ */
+static void guac_rdp_rdpgfx_channel_disconnected(rdpContext* context,
+        ChannelDisconnectedEventArgs* args) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Ignore disconnection event if it's not for the RDPGFX channel */
+    if (strcmp(args->name, RDPGFX_DVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* Un-init GDI-backed support for the Graphics Pipeline */
+    RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface;
+    rdpGdi* gdi = context->gdi;
+    gdi_graphics_pipeline_uninit(gdi, rdpgfx);
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel support unloaded.");
+
+}
+
+void guac_rdp_rdpgfx_load_plugin(rdpContext* context) {
+
+    /* Subscribe to and handle channel connected events */
+    PubSub_SubscribeChannelConnected(context->pubSub,
+        (pChannelConnectedEventHandler) guac_rdp_rdpgfx_channel_connected);
+
+    /* Subscribe to and handle channel disconnected events */
+    PubSub_SubscribeChannelDisconnected(context->pubSub,
+        (pChannelDisconnectedEventHandler) guac_rdp_rdpgfx_channel_disconnected);
+
+    /* Add "rdpgfx" channel */
+    guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpgfx", NULL);
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpgfx.h b/src/protocols/rdp/channels/rdpgfx.h
new file mode 100644
index 0000000..993445c
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpgfx.h
@@ -0,0 +1,49 @@
+/*
+ * 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_CHANNELS_RDPGFX_H
+#define GUAC_RDP_CHANNELS_RDPGFX_H
+
+#include "settings.h"
+
+#include <freerdp/client/rdpgfx.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+
+/**
+ * Adds FreeRDP's "rdpgfx" plugin to the list of dynamic virtual channel plugins
+ * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will
+ * automatically be associated with the guac_rdp_rdpgfx instance pointed to by the
+ * current guac_rdp_client. The plugin will only be loaded once the "drdynvc"
+ * plugin is loaded. The "rdpgfx" plugin ultimately adds support for the RDP
+ * Graphics Pipeline Extension.
+ *
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for Graphics Pipeline support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ */
+void guac_rdp_rdpgfx_load_plugin(rdpContext* context);
+
+#endif
+
diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c
index 8ddc567..aeb7c5a 100644
--- a/src/protocols/rdp/gdi.c
+++ b/src/protocols/rdp/gdi.c
@@ -26,6 +26,7 @@
 
 #include <cairo/cairo.h>
 #include <freerdp/freerdp.h>
+#include <freerdp/gdi/gdi.h>
 #include <freerdp/graphics.h>
 #include <freerdp/primary.h>
 #include <guacamole/client.h>
@@ -33,7 +34,6 @@
 #include <winpr/wtypes.h>
 
 #include <stddef.h>
-#include <stddef.h>
 
 guac_transfer_function guac_rdp_rop3_transfer_function(guac_client* client,
         int rop3) {
@@ -371,11 +371,112 @@
 
 }
 
-BOOL guac_rdp_gdi_end_paint(rdpContext* context) {
-    /* IGNORE */
+void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* The server supports defining explicit frames */
+    rdp_client->frames_supported = 1;
+
+    /* A new frame is beginning */
+    if (starting) {
+        rdp_client->in_frame = 1;
+        return;
+    }
+
+    /* The current frame has ended */
+    guac_timestamp frame_end = guac_timestamp_current();
+    int time_elapsed = frame_end - client->last_sent_timestamp;
+    rdp_client->in_frame = 0;
+
+    /* A new frame has been received from the RDP server and processed */
+    rdp_client->frames_received++;
+
+    /* Flush a new frame if the client is ready for it */
+    if (time_elapsed >= guac_client_get_processing_lag(client)) {
+        guac_common_display_flush(rdp_client->display);
+        guac_client_end_multiple_frames(client, rdp_client->frames_received);
+        guac_socket_flush(client->socket);
+        rdp_client->frames_received = 0;
+    }
+
+}
+
+BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* frame_marker) {
+    guac_rdp_gdi_mark_frame(context, frame_marker->action == FRAME_START);
     return TRUE;
 }
 
+BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker) {
+
+    guac_rdp_gdi_mark_frame(context, surface_frame_marker->frameAction != SURFACECMD_FRAMEACTION_END);
+
+    if (context->settings->FrameAcknowledge > 0)
+        IFCALL(context->update->SurfaceFrameAcknowledge, context,
+                surface_frame_marker->frameId);
+
+    return TRUE;
+
+}
+
+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;
+    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;
+
+    /* Ignore paint if nothing has been done (empty rect) */
+    if (gdi->primary->hdc->hwnd->invalid->null)
+        return TRUE;
+
+    INT32 x = gdi->primary->hdc->hwnd->invalid->x;
+    INT32 y = gdi->primary->hdc->hwnd->invalid->y;
+    UINT32 w = gdi->primary->hdc->hwnd->invalid->w;
+    UINT32 h = gdi->primary->hdc->hwnd->invalid->h;
+
+    /* Create surface from image data */
+    cairo_surface_t* surface = cairo_image_surface_create_for_data(
+        gdi->primary_buffer + 4*x + y*gdi->stride,
+        CAIRO_FORMAT_RGB24, w, h, gdi->stride);
+
+    /* Send surface to buffer */
+    guac_common_surface_draw(rdp_client->display->default_surface, x, y, surface);
+
+    /* Free surface */
+    cairo_surface_destroy(surface);
+
+    /* Next frame */
+    if (gdi->inGfxFrame) {
+        guac_rdp_gdi_mark_frame(context, 0);
+    }
+
+    return TRUE;
+
+}
+
 BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
@@ -391,8 +492,7 @@
             guac_rdp_get_width(context->instance),
             guac_rdp_get_height(context->instance));
 
-    return TRUE;
+    return gdi_resize(context->gdi, guac_rdp_get_width(context->instance),
+            guac_rdp_get_height(context->instance));
 
 }
-
-
diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h
index 76735aa..955bf7b 100644
--- a/src/protocols/rdp/gdi.h
+++ b/src/protocols/rdp/gdi.h
@@ -156,8 +156,68 @@
 BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds);
 
 /**
- * Handler called when a paint operation is complete. We don't actually
- * use this, but FreeRDP requires it. Calling this function has no effect.
+ * 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
+ * a new frame, a new frame will be flushed to the client.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param starting
+ *     Non-zero if the frame in question is starting, zero if the frame is
+ *     ending.
+ */
+void guac_rdp_gdi_mark_frame(rdpContext* context, int starting);
+
+/**
+ * Handler called when a frame boundary is received from the RDP server in the
+ * form of a frame marker command. Each frame boundary may be the beginning or
+ * the end of a frame.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param frame_marker
+ *     The received frame marker.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
+ */
+BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* frame_marker);
+
+/**
+ * Handler called when a frame boundary is received from the RDP server in the
+ * form of a surface frame marker. Each frame boundary may be the beginning or
+ * the end of a frame.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param surface_frame_marker
+ *     The received frame marker.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
+ */
+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.
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
diff --git a/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap b/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap
new file mode 100644
index 0000000..b9bec80
--- /dev/null
+++ b/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap
@@ -0,0 +1,79 @@
+#
+# 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.
+#
+
+parent  "base"
+name    "cs-cz-qwertz"
+freerdp "KBD_CZECH"
+
+#
+# Basic keys
+#
+
+map -caps -altgr -shift 0x29 0x02..0x0D      ~ ";+ěščřžýáíé=´"
+map -caps -altgr -shift      0x10..0x1B      ~ "qwertzuıopú)"
+map -caps -altgr -shift      0x1E..0x28 0x2B ~ "asdfghjklů§¨"
+map -caps -altgr -shift      0x2C..0x35      ~ "yxcvbnm,.-"
+
+map -caps -altgr +shift 0x29 0x02..0x0D      ~ "°1234567890%ˇ"
+map -caps -altgr +shift      0x10..0x1B      ~ "QWERTZUIOP/("
+map -caps -altgr +shift      0x1E..0x28 0x2B ~ "ASDFGHJKL"!'"
+map -caps -altgr +shift      0x2C..0x35      ~ "YXCVBNM?:_"
+
+map +caps -altgr -shift 0x29 0x02..0x0D      ~ ";+ĚŠČŘŽÝÁÍÉ=´"
+map +caps -altgr -shift      0x10..0x1B      ~ "QWERTZUIOPÚ)"
+map +caps -altgr -shift      0x1E..0x28 0x2B ~ "ASDFGHJKL٧¨"
+map +caps -altgr -shift      0x2C..0x35      ~ "YXCVBNM,.-"
+
+map +caps -altgr +shift 0x29 0x02..0x0D      ~ "°1234567890%ˇ"
+map +caps -altgr +shift      0x10..0x1B      ~ "qwertzuiop/("
+map +caps -altgr +shift      0x1E..0x28 0x2B ~ "asdfghjkl"!'"
+map +caps -altgr +shift      0x2C..0x35      ~ "yxcvbnm?:_"
+
+#
+# Keys requiring AltGr
+#
+
+map +altgr -shift 0x02 ~ "~"
+
+map +altgr -shift 0x10 ~ "\"
+map +altgr -shift 0x11 ~ "|"
+map +altgr -shift 0x12 ~ "€"
+map +altgr -shift 0x1A ~ "÷"
+map +altgr -shift 0x1B ~ "×"
+
+map +altgr -shift 0x1F ~ "đ"
+map +altgr -shift 0x20 ~ "Đ"
+map +altgr -shift 0x21 ~ "["
+map +altgr -shift 0x22 ~ "]"
+map +altgr -shift 0x25 ~ "ł"
+map +altgr -shift 0x26 ~ "Ł"
+map +altgr -shift 0x27 ~ "$"
+map +altgr -shift 0x28 ~ "ß"
+map +altgr -shift 0x2B ~ "¤"
+
+map +altgr -shift 0x2D ~ "#"
+map +altgr -shift 0x2E ~ "&"
+map +altgr -shift 0x2F ~ "@"
+map +altgr -shift 0x30 ~ "{"
+map +altgr -shift 0x31 ~ "}"
+map +altgr -shift 0x33 ~ "<"
+map +altgr -shift 0x34 ~ ">"
+map +altgr -shift 0x35 ~ "*"
+
+# END
diff --git a/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap b/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap
new file mode 100644
index 0000000..ee79f82
--- /dev/null
+++ b/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap
@@ -0,0 +1,55 @@
+#
+# 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.
+#
+
+parent  "base"
+name    "fr-ca-qwerty"
+freerdp "KBD_CANADIAN_FRENCH"
+
+#
+# Basic keys
+#
+
+map -altgr -shift 0x29 0x02..0x0D      ~ "#1234567890-="
+map -altgr -shift      0x10..0x1B 0x2B ~ "qwertyuiop^¸<"
+map -altgr -shift      0x1E..0x28      ~ "asdfghjkl;`"
+map -altgr -shift      0x2C..0x35      ~ "zxcvbnm,.é"
+
+map -altgr +shift 0x29 0x02..0x0D      ~ "|!"/$%?&*()_+"
+map -altgr +shift      0x10..0x1B 0x2B ~ "QWERTYUIOP^¨>"
+map -altgr +shift      0x1E..0x28      ~ "ASDFGHJKL:`"
+map -altgr +shift      0x2C..0x35      ~ "ZXCVBNM'.É"
+
+#
+# Keys requiring AltGr
+#
+
+map +altgr -shift 0x29 0x02..0x0D      ~ "\±@£¢¤¬¦²³¼½¾"
+map +altgr -shift      0x18..0x1B 0x2B ~ "§¶[]}"
+map +altgr -shift      0x27..0x28      ~ "~{"
+map +altgr -shift      0x32..0x33 0x35 ~ "µ¯´"
+
+#
+# Combined accents
+#
+
+map -altgr -shift 0x1A ~ 0x0302 # COMBINING CIRCUMFLEX ACCENT
+map -altgr -shift 0x1B ~ 0x0327 # COMBINING CEDILLA
+map -altgr +shift 0x1B ~ 0x0308 # COMBINING DIAERESIS
+map -altgr -shift 0x28 ~ 0x0300 # COMBINING GRAVE ACCENT
+map +altgr -shift 0x35 ~ 0x0301 # COMBINING ACUTE ACCENT
diff --git a/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap b/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap
new file mode 100644
index 0000000..5bfbee8
--- /dev/null
+++ b/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap
@@ -0,0 +1,69 @@
+#
+# 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.
+#
+
+parent  "base"
+name    "pt-pt-qwerty"
+freerdp "KBD_PORTUGUESE"
+
+#
+# Basic keys
+#
+
+map -caps -shift 0x29 0x02..0x0D ~ "\1234567890'«"
+map -caps -shift      0x10..0x1A ~ "qwertyuiop+"
+map -caps -shift      0x1E..0x28 ~ "asdfghjklçº"
+map -caps -shift 0x56 0x2C..0x35 ~ "<zxcvbnm,.-"
+
+map -caps +shift 0x29 0x02..0x0D ~ "|!"#$%&/()=?»"
+map -caps +shift      0x10..0x1A ~ "QWERTYUIOP*"
+map -caps +shift      0x1E..0x28 ~ "ASDFGHJKLǪ"
+map -caps +shift 0x56 0x2C..0x35 ~ ">ZXCVBNM;:_"
+
+map +caps -shift 0x29 0x02..0x0D ~ "\1234567890'«"
+map +caps -shift      0x10..0x1A ~ "QWERTYUIOP+"
+map +caps -shift      0x1E..0x28 ~ "ASDFGHJKLǺ"
+map +caps -shift 0x56 0x2C..0x35 ~ "\ZXCVBNM,./"
+
+map +caps +shift 0x29 0x02..0x0D ~ "|!"#$%&/()=?»"
+map +caps +shift      0x10..0x1A ~ "qwertyuiop*"
+map +caps +shift      0x1E..0x28 ~ "asdfghjklçª"
+map +caps +shift 0x56 0x2C..0x35 ~ ">zxcvbnm;:_"
+
+
+#
+# Keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed)
+#
+
+map +altgr -shift 0x03..0x0B ~ "@£§½¬{[]}"
+map +altgr -shift 0x12       ~ "€"
+
+#
+# Dead keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed)
+#
+
+map +altgr -shift 0x1A ~ 0xFE57 # Dead diaeresis
+
+#
+# Dead keys (unaffected by Caps Lock, but AltGr must not be pressed)
+#
+
+map -altgr -shift 0x1B ~ 0xFE51 # Dead acute
+map -altgr +shift 0x1B ~ 0xFE50 # Dead grave
+map -altgr -shift 0x2B ~ 0xFE53 # Dead tilde
+map -altgr +shift 0x2B ~ 0xFE52 # Dead circumflex
diff --git a/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap b/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap
new file mode 100644
index 0000000..51996fd
--- /dev/null
+++ b/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap
@@ -0,0 +1,92 @@
+#
+# 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.
+#
+
+parent  "base"
+name    "ro-ro-qwerty"
+freerdp "KBD_ROMANIAN"
+
+#
+# Basic keys
+#
+
+map -caps -shift 0x29 0x02..0x0D      ~ "`1234567890-="
+map -caps -shift      0x10..0x1B 0x2B ~ "qwertyuiop[]\"
+map -caps -shift      0x1E..0x28      ~ "asdfghjkl;'"
+map -caps -shift 0x56 0x2C..0x35      ~ "\zxcvbnm,./"
+
+map -caps +shift 0x29 0x02..0x0D      ~ "~!@#$%^&*()_+"
+map -caps +shift      0x10..0x1B 0x2B ~ "QWERTYUIOP{}|"
+map -caps +shift      0x1E..0x28      ~ "ASDFGHJKL:""
+map -caps +shift 0x56 0x2C..0x35      ~ "|ZXCVBNM<>?"
+
+map +caps -shift 0x29 0x02..0x0D      ~ "`1234567890-="
+map +caps -shift      0x10..0x1B 0x2B ~ "QWERTYUIOP[]\"
+map +caps -shift      0x1E..0x28      ~ "ASDFGHJKL;'"
+map +caps -shift 0x56 0x2C..0x35      ~ "\ZXCVBNM,./"
+
+map +caps +shift 0x29 0x02..0x0D      ~ "~!@#$%^&*()_+"
+map +caps +shift      0x10..0x1B 0x2B ~ "qwertyuiop{}|"
+map +caps +shift      0x1E..0x28      ~ "asdfghjkl:""
+map +caps +shift 0x56 0x2C..0x35      ~ "|zxcvbnm<>?"
+
+#
+# Keys requiring AltGr (unaffected by Caps Lock)
+#
+
+map +altgr +shift 0x0C ~ "–"
+map +altgr +shift 0x0D ~ "±"
+map +altgr -shift 0x10 ~ "â"
+map +altgr +shift 0x10 ~ "Â"
+map +altgr -shift 0x11 ~ "ß"
+map +altgr -shift 0x12 ~ "€"
+map +altgr -shift 0x14 ~ "ț"
+map +altgr +shift 0x14 ~ "Ț"
+map +altgr -shift 0x17 ~ "î"
+map +altgr +shift 0x17 ~ "Î"
+map +altgr -shift 0x19 ~ "§"
+map +altgr -shift 0x1A ~ "„"
+map +altgr -shift 0x1B ~ "”"
+map +altgr -shift 0x1E ~ "ă"
+map +altgr +shift 0x1E ~ "Ă"
+map +altgr -shift 0x1F ~ "ș"
+map +altgr +shift 0x1F ~ "Ș"
+map +altgr -shift 0x20 ~ "đ"
+map +altgr +shift 0x20 ~ "Đ"
+map +altgr -shift 0x26 ~ "ł"
+map +altgr +shift 0x26 ~ "Ł"
+map +altgr -shift 0x2E ~ "©"
+map +altgr -shift 0x33 ~ "«"
+map +altgr -shift 0x34 ~ "»"
+
+#
+# Dead keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed)
+#
+
+map +altgr -shift 0x02 ~ 0xFE53 # Dead tilde
+map +altgr -shift 0x03 ~ 0xFE5A # Dead caron
+map +altgr -shift 0x04 ~ 0xFE52 # Dead circumflex
+map +altgr -shift 0x05 ~ 0xFE55 # Dead breve
+map +altgr -shift 0x06 ~ 0xFE58 # Dead abovering
+map +altgr -shift 0x07 ~ 0xFE5C # Dead ogonek
+map +altgr -shift 0x08 ~ 0xFE50 # Dead grave
+map +altgr -shift 0x09 ~ 0xFE56 # Dead abovedot
+map +altgr -shift 0x0A ~ 0xFE51 # Dead acute
+map +altgr -shift 0x0B ~ 0xFE59 # Dead doubleacute
+map +altgr -shift 0x0C ~ 0xFE57 # Dead diaeresis
+map +altgr -shift 0x0D ~ 0xFE5B # Dead cedilla
diff --git a/src/protocols/rdp/pointer.c b/src/protocols/rdp/pointer.c
index 1d8d7a0..ae167cf 100644
--- a/src/protocols/rdp/pointer.c
+++ b/src/protocols/rdp/pointer.c
@@ -21,6 +21,7 @@
 #include "common/cursor.h"
 #include "common/display.h"
 #include "common/surface.h"
+#include "gdi.h"
 #include "pointer.h"
 #include "rdp.h"
 
@@ -77,11 +78,22 @@
     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 */
     guac_common_cursor_set_surface(rdp_client->display->cursor,
             pointer->xPos, pointer->yPos,
             ((guac_rdp_pointer*) pointer)->layer->surface);
 
+    if (rdp_client->frames_supported && !in_frame)
+        guac_rdp_gdi_mark_frame(context, 0);
+
     return TRUE;
 
 }
@@ -105,9 +117,20 @@
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
 
+    /* Add explicit frame boundaries around cursor set operation if not already
+     * in a frame (the RDP protocol does not send nor expect frame boundaries
+     * for cursor changes, but Guacamole does expect this) */
+    int in_frame = rdp_client->in_frame;
+
+    if (rdp_client->frames_supported && !in_frame)
+        guac_rdp_gdi_mark_frame(context, 1);
+
     /* Set cursor to empty/blank graphic */
     guac_common_cursor_set_blank(rdp_client->display->cursor);
 
+    if (rdp_client->frames_supported && !in_frame)
+        guac_rdp_gdi_mark_frame(context, 0);
+
     return TRUE;
 
 }
@@ -117,9 +140,20 @@
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
 
+    /* Add explicit frame boundaries around cursor set operation if not already
+     * in a frame (the RDP protocol does not send nor expect frame boundaries
+     * for cursor changes, but Guacamole does expect this) */
+    int in_frame = rdp_client->in_frame;
+
+    if (rdp_client->frames_supported && !in_frame)
+        guac_rdp_gdi_mark_frame(context, 1);
+
     /* Set cursor to embedded pointer */
     guac_common_cursor_set_pointer(rdp_client->display->cursor);
 
+    if (rdp_client->frames_supported && !in_frame)
+        guac_rdp_gdi_mark_frame(context, 0);
+
     return TRUE;
 }
 
diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c
index 5905f8e..b60db48 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -28,6 +28,7 @@
 #include "channels/rail.h"
 #include "channels/rdpdr/rdpdr.h"
 #include "channels/rdpei.h"
+#include "channels/rdpgfx.h"
 #include "channels/rdpsnd/rdpsnd.h"
 #include "client.h"
 #include "color.h"
@@ -137,15 +138,6 @@
 
     }
 
-    /* Load plugin providing Dynamic Virtual Channel support, if required */
-    if (instance->settings->SupportDynamicChannels &&
-            guac_freerdp_channels_load_plugin(context, "drdynvc",
-                instance->settings)) {
-        guac_client_log(client, GUAC_LOG_WARNING,
-                "Failed to load drdynvc plugin. Display update and audio "
-                "input support will be disabled.");
-    }
-
     /* Init FreeRDP internal GDI implementation */
     if (!gdi_init(instance, guac_rdp_get_native_pixel_format(FALSE)))
         return FALSE;
@@ -187,9 +179,13 @@
 
     /* Set up GDI */
     instance->update->DesktopResize = guac_rdp_gdi_desktop_resize;
+    instance->update->BeginPaint = guac_rdp_gdi_begin_paint;
     instance->update->EndPaint = guac_rdp_gdi_end_paint;
     instance->update->SetBounds = guac_rdp_gdi_set_bounds;
 
+    instance->update->SurfaceFrameMarker = guac_rdp_gdi_surface_frame_marker;
+    instance->update->altsec->FrameMarker = guac_rdp_gdi_frame_marker;
+
     rdpPrimaryUpdate* primary = instance->update->primary;
     primary->DstBlt = guac_rdp_gdi_dstblt;
     primary->PatBlt = guac_rdp_gdi_patblt;
@@ -204,6 +200,19 @@
     offscreen_cache_register_callbacks(instance->update);
     palette_cache_register_callbacks(instance->update);
 
+    /* Load "rdpgfx" plugin for Graphics Pipeline Extension */
+    if (settings->enable_gfx)
+        guac_rdp_rdpgfx_load_plugin(context);
+
+    /* Load plugin providing Dynamic Virtual Channel support, if required */
+    if (instance->settings->SupportDynamicChannels &&
+            guac_freerdp_channels_load_plugin(context, "drdynvc",
+                instance->settings)) {
+        guac_client_log(client, GUAC_LOG_WARNING,
+                "Failed to load drdynvc plugin. Display update and audio "
+                "input support will be disabled.");
+    }
+
     return TRUE;
 
 }
@@ -524,8 +533,6 @@
     /* Connection complete */
     rdp_client->rdp_inst = rdp_inst;
 
-    guac_timestamp last_frame_end = guac_timestamp_current();
-
     /* Signal that reconnect has been completed */
     guac_rdp_disp_reconnect_complete(rdp_client->disp);
 
@@ -544,7 +551,6 @@
         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 {
@@ -564,13 +570,21 @@
                     break;
                 }
 
+                /* Continue handling inbound data if we are in the middle of an RDP frame */
+                if (rdp_client->in_frame) {
+                    wait_result = rdp_guac_client_wait_for_messages(client, GUAC_RDP_FRAME_START_TIMEOUT);
+                    if (wait_result >= 0)
+                        continue;
+                }
+
                 /* Calculate time remaining in frame */
+                guac_timestamp frame_start = client->last_sent_timestamp;
                 frame_end = guac_timestamp_current();
                 frame_remaining = frame_start + GUAC_RDP_FRAME_DURATION
                                 - frame_end;
 
                 /* Calculate time that client needs to catch up */
-                int time_elapsed = frame_end - last_frame_end;
+                int time_elapsed = frame_end - frame_start;
                 int required_wait = processing_lag - time_elapsed;
 
                 /* Increase the duration of this frame if client is lagging */
@@ -587,12 +601,6 @@
 
             } 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;
-
         }
 
         /* Test whether the RDP server is closing the connection */
@@ -607,11 +615,13 @@
             guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE,
                     "Connection closed.");
 
-        /* Flush frame only if successful */
-        else {
+        /* 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_frame(client);
+            guac_client_end_multiple_frames(client, rdp_client->frames_received);
             guac_socket_flush(client->socket);
+            rdp_client->frames_received = 0;
         }
 
     }
@@ -825,4 +835,3 @@
     return NULL;
 
 }
-
diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h
index 9daef80..9e3d3fe 100644
--- a/src/protocols/rdp/rdp.h
+++ b/src/protocols/rdp/rdp.h
@@ -92,6 +92,24 @@
     guac_common_surface* 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.
+     */
+    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;
+
+    /**
      * The current state of the keyboard with respect to the RDP session.
      */
     guac_rdp_keyboard* keyboard;
diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c
index c286c71..f2dbeab 100644
--- a/src/protocols/rdp/settings.c
+++ b/src/protocols/rdp/settings.c
@@ -91,6 +91,7 @@
     "disable-bitmap-caching",
     "disable-offscreen-caching",
     "disable-glyph-caching",
+    "disable-gfx",
     "preconnection-id",
     "preconnection-blob",
     "timezone",
@@ -383,6 +384,13 @@
     IDX_DISABLE_GLYPH_CACHING,
 
     /**
+     * "true" if the RDP Graphics Pipeline Extension should not be used, and
+     * traditional RDP graphics should be used instead, "false" or blank if the
+     * Graphics Pipeline Extension should be used if available.
+     */
+    IDX_DISABLE_GFX,
+
+    /**
      * The preconnection ID to send within the preconnection PDU when
      * initiating an RDP connection, if any.
      */
@@ -934,11 +942,6 @@
                 GUAC_RDP_CLIENT_ARGS[IDX_DISABLE_GLYPH_CACHING]);
     }
 
-    /* Session color depth */
-    settings->color_depth = 
-        guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv,
-                IDX_COLOR_DEPTH, RDP_DEFAULT_DEPTH);
-
     /* Preconnection ID */
     settings->preconnection_id = -1;
     if (argv[IDX_PRECONNECTION_ID][0] != '\0') {
@@ -1155,6 +1158,16 @@
         settings->resize_method = GUAC_RESIZE_NONE;
     }
 
+    /* RDP Graphics Pipeline enable/disable */
+    settings->enable_gfx =
+        !guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
+                IDX_DISABLE_GFX, 0);
+
+    /* Session color depth */
+    settings->color_depth =
+        guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv,
+                IDX_COLOR_DEPTH, settings->enable_gfx ? RDP_GFX_REQUIRED_DEPTH : RDP_DEFAULT_DEPTH);
+
     /* Multi-touch input enable/disable */
     settings->enable_touch =
         guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
@@ -1423,6 +1436,29 @@
     /* Explicitly set flag value */
     rdp_settings->PerformanceFlags = guac_rdp_get_performance_flags(guac_settings);
 
+    /* Always request frame markers */
+    rdp_settings->FrameMarkerCommandEnabled = TRUE;
+    rdp_settings->SurfaceFrameMarkerEnabled = TRUE;
+
+    /* Enable RemoteFX / Graphics Pipeline */
+    if (guac_settings->enable_gfx) {
+
+        rdp_settings->SupportGraphicsPipeline = TRUE;
+        rdp_settings->RemoteFxCodec = TRUE;
+
+        if (rdp_settings->ColorDepth != RDP_GFX_REQUIRED_DEPTH) {
+            guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "
+                    "color depth of %i bpp, as the RDP Graphics Pipeline "
+                    "requires %i bpp.", rdp_settings->ColorDepth, RDP_GFX_REQUIRED_DEPTH);
+        }
+
+        /* Required for RemoteFX / Graphics Pipeline */
+        rdp_settings->FastPathOutput = TRUE;
+        rdp_settings->ColorDepth = RDP_GFX_REQUIRED_DEPTH;
+        rdp_settings->SoftwareGdi = TRUE;
+
+    }
+
     /* Set individual flags - some FreeRDP versions overwrite the above */
     rdp_settings->AllowFontSmoothing = guac_settings->font_smoothing_enabled;
     rdp_settings->DisableWallpaper = !guac_settings->wallpaper_enabled;
diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h
index daaf3e9..3fe9d00 100644
--- a/src/protocols/rdp/settings.h
+++ b/src/protocols/rdp/settings.h
@@ -59,6 +59,11 @@
 #define RDP_DEFAULT_DEPTH  16 
 
 /**
+ * The color depth required by the RDPGFX channel, in bits.
+ */
+#define RDP_GFX_REQUIRED_DEPTH 32
+
+/**
  * The filename to use for the screen recording, if not specified.
  */
 #define GUAC_RDP_DEFAULT_RECORDING_NAME "recording"
@@ -553,6 +558,11 @@
     int enable_audio_input;
 
     /**
+     * Whether the RDP Graphics Pipeline Extension is enabled.
+     */
+    int enable_gfx;
+
+    /**
      * Whether multi-touch support is enabled.
      */
     int enable_touch;
diff --git a/src/terminal/display.c b/src/terminal/display.c
index a9d671f..d449325 100644
--- a/src/terminal/display.c
+++ b/src/terminal/display.c
@@ -202,6 +202,19 @@
 
 }
 
+/**
+ * Calculate the size of margins around the terminal based on DPI.
+ *
+ * @param dpi
+ *     The resolution of the display in DPI.
+ *
+ * @return
+ *     Calculated size of margin in pixels.
+ */
+static int get_margin_by_dpi(int dpi) {
+    return dpi * GUAC_TERMINAL_MARGINS / GUAC_TERMINAL_MM_PER_INCH;
+}
+
 guac_terminal_display* guac_terminal_display_alloc(guac_client* client,
         const char* font_name, int font_size, int dpi,
         guac_terminal_color* foreground, guac_terminal_color* background,
@@ -229,6 +242,13 @@
     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_protocol_send_move(client->socket, display->display_layer,
+        GUAC_DEFAULT_LAYER, display->margin, display->margin, 0);
+
     display->default_foreground = display->glyph_foreground = *foreground;
     display->default_background = display->glyph_background = *background;
     display->default_palette = palette;
@@ -447,6 +467,10 @@
 
 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;
 
@@ -828,6 +852,10 @@
     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,
@@ -1020,7 +1048,7 @@
     if (new_width != display->width || new_height != display->height)
         guac_terminal_display_resize(display, new_width, new_height);
 
+
     return 0;
 
-}
-
+}
\ No newline at end of file
diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c
index 6e66006..9d9c418 100644
--- a/src/terminal/terminal.c
+++ b/src/terminal/terminal.c
@@ -20,6 +20,7 @@
 
 #include "common/clipboard.h"
 #include "common/cursor.h"
+#include "common/iconv.h"
 #include "terminal/buffer.h"
 #include "terminal/color-scheme.h"
 #include "terminal/common.h"
@@ -335,6 +336,97 @@
     return options;
 }
 
+/**
+ * Calculate the available height and width in characters for text display in 
+ * the terminal and store the results in the pointer arguments.
+ *
+ * @param terminal
+ *     The terminal provides character width and height for calculations.
+ * 
+ * @param height
+ *     The outer height of the terminal, in pixels.
+ * 
+ * @param width
+ *     The outer width of the terminal, in pixels.
+ * 
+ * @param rows
+ *     Pointer to the calculated height of the terminal for text display,
+ *     in characters.
+ * 
+ * @param columns
+ *     Pointer to the calculated width of the terminal for text display,
+ *     in characters.
+ */
+static void calculate_rows_and_columns(guac_terminal* term,
+    int height, int width, int *rows, int *columns) {
+
+    int margin = term->display->margin;
+    int char_width = term->display->char_width;
+    int char_height = term->display->char_height;
+    
+    /* Calculate available display area */
+    int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH - 2 * margin;
+    if (available_width < 0)
+        available_width = 0;
+
+    int available_height = height - 2 * margin;
+    if (available_height < 0)
+        available_height = 0;
+
+    /* Calculate dimensions */
+    *rows    = available_height / char_height;
+    *columns = available_width / char_width;
+
+    /* Keep height within predefined maximum */
+    if (*rows > GUAC_TERMINAL_MAX_ROWS)
+        *rows = GUAC_TERMINAL_MAX_ROWS;
+
+    /* Keep width within predefined maximum */
+    if (*columns > GUAC_TERMINAL_MAX_COLUMNS)
+        *columns = GUAC_TERMINAL_MAX_COLUMNS;
+}
+
+/**
+ * Calculate the available height and width in pixels of the terminal for text 
+ * display in the terminal and store the results in the pointer arguments.
+ *
+ * @param terminal
+ *     The terminal provides character width and height for calculations.
+ * 
+ * @param rows
+ *     The available height of the terminal for text display, in characters.
+ * 
+ * @param columns
+ *     The available width of the terminal for text display, in characters.
+ *
+ * @param height
+ *     Pointer to the calculated available height of the terminal for text 
+ *     display, in pixels.
+ * 
+ * @param width
+ *     Pointer to the calculated available width of the terminal for text 
+ *     display, in pixels.
+ */
+static void calculate_height_and_width(guac_terminal* term,
+    int rows, int columns, int *height, int *width) {
+
+    int margin = term->display->margin;
+    int char_width = term->display->char_width;
+    int char_height = term->display->char_height;
+
+    /* Recalculate height if max rows reached */
+    if (rows == GUAC_TERMINAL_MAX_ROWS) {
+        int available_height = GUAC_TERMINAL_MAX_ROWS * char_height;
+        *height = available_height + 2 * margin;
+    }
+
+    /* Recalculate width if max columns reached */
+    if (columns == GUAC_TERMINAL_MAX_COLUMNS) {
+        int available_width = GUAC_TERMINAL_MAX_COLUMNS * char_width;
+        *width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH + 2 * margin;
+    }
+}
+
 guac_terminal* guac_terminal_create(guac_client* client,
         guac_terminal_options* options) {
 
@@ -363,11 +455,6 @@
                                      &default_char.attributes.background,
                                      default_palette);
 
-    /* Calculate available display area */
-    int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH;
-    if (available_width < 0)
-        available_width = 0;
-
     guac_terminal* term = malloc(sizeof(guac_terminal));
     term->started = false;
     term->client = client;
@@ -379,10 +466,6 @@
     term->font_name = strdup(options->font_name);
     term->font_size = options->font_size;
 
-    /* Set size of available screen area */
-    term->outer_width = width;
-    term->outer_height = height;
-
     /* Init modified flag and conditional */
     term->modified = 0;
     pthread_cond_init(&(term->modified_cond), NULL);
@@ -425,29 +508,27 @@
     term->clipboard = guac_common_clipboard_alloc();
     term->disable_copy = options->disable_copy;
 
-    /* Calculate character size */
-    int rows    = height / term->display->char_height;
-    int columns = available_width / term->display->char_width;
+    /* Calculate available text display area by character size */
+    int rows, columns;
+    calculate_rows_and_columns(term, height, width, &rows, &columns);
 
-    /* Keep height within predefined maximum */
-    if (rows > GUAC_TERMINAL_MAX_ROWS) {
-        rows = GUAC_TERMINAL_MAX_ROWS;
-        height = rows * term->display->char_height;
-    }
+    /* Calculate available display area in pixels */
+    int adjusted_height = height; 
+    int adjusted_width = width;
+    calculate_height_and_width(term, rows, columns,
+        &adjusted_height, &adjusted_width);
 
-    /* Keep width within predefined maximum */
-    if (columns > GUAC_TERMINAL_MAX_COLUMNS) {
-        columns = GUAC_TERMINAL_MAX_COLUMNS;
-        available_width = columns * term->display->char_width;
-        width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH;
-    }
+    /* Set size of available screen area */
+    term->outer_height = height;
+    term->outer_width = width;
+
+    /* Set rows and columns size */
+    term->term_height = rows;
+    term->term_width  = columns;
 
     /* Set pixel size */
-    term->width = width;
-    term->height = height;
-
-    term->term_width  = columns;
-    term->term_height = rows;
+    term->height = adjusted_height;
+    term->width = adjusted_width;
 
     /* Open STDIN pipe */
     if (pipe(term->stdin_pipe_fd)) {
@@ -476,7 +557,7 @@
 
     /* Allocate scrollbar */
     term->scrollbar = guac_terminal_scrollbar_alloc(term->client, GUAC_DEFAULT_LAYER,
-            width, height, term->term_height);
+            term->outer_width, term->outer_height, term->term_height);
 
     /* Associate scrollbar with this terminal */
     term->scrollbar->data = term;
@@ -1302,7 +1383,7 @@
     guac_terminal_display_flush(term->display);
     guac_terminal_display_resize(term->display, width, height);
 
-    /* Reraw any characters on right if widening */
+    /* 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);
 
@@ -1385,55 +1466,39 @@
     /* Acquire exclusive access to terminal */
     guac_terminal_lock(terminal);
 
+    /* Calculate available text display area by character size */
+    int rows, columns;
+    calculate_rows_and_columns(terminal, height, width, &rows, &columns);
+
+    /* Calculate available display area in pixels */
+    int adjusted_height = height; 
+    int adjusted_width = width;
+    calculate_height_and_width(terminal, rows, columns,
+        &adjusted_height, &adjusted_width);
+
     /* Set size of available screen area */
-    terminal->outer_width = width;
     terminal->outer_height = height;
+    terminal->outer_width = width;
 
-    /* Calculate available display area */
-    int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH;
-    if (available_width < 0)
-        available_width = 0;
-
-    /* Calculate dimensions */
-    int rows    = height / display->char_height;
-    int columns = available_width / display->char_width;
-
-    /* Keep height within predefined maximum */
-    if (rows > GUAC_TERMINAL_MAX_ROWS) {
-        rows = GUAC_TERMINAL_MAX_ROWS;
-        height = rows * display->char_height;
-    }
-
-    /* Keep width within predefined maximum */
-    if (columns > GUAC_TERMINAL_MAX_COLUMNS) {
-        columns = GUAC_TERMINAL_MAX_COLUMNS;
-        available_width = columns * display->char_width;
-        width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH;
-    }
-
-    /* Set pixel sizes */
-    terminal->width = width;
-    terminal->height = height;
+    /* Set pixel size */
+    terminal->height = adjusted_height;
+    terminal->width = adjusted_width;
 
     /* Resize default layer to given pixel dimensions */
     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) {
-
-        guac_client_log(client, GUAC_LOG_DEBUG,
-                "Resizing terminal to %ix%i", rows, columns);
-
-        /* Resize terminal */
+        /* 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 */
-    guac_terminal_scrollbar_parent_resized(terminal->scrollbar, width, height, rows);
+    guac_terminal_scrollbar_parent_resized(terminal->scrollbar, 
+        terminal->outer_width, terminal->outer_height, terminal->term_height);
     guac_terminal_scrollbar_set_bounds(terminal->scrollbar,
             -guac_terminal_get_available_scroll(terminal), 0);
 
@@ -2100,7 +2165,16 @@
 
 void guac_terminal_clipboard_append(guac_terminal* terminal,
         const char* data, int length) {
-    guac_common_clipboard_append(terminal->clipboard, data, length);
+
+    /* Allocate and clear space for the converted data */
+    char output_data[GUAC_COMMON_CLIPBOARD_MAX_LENGTH];
+    char* output = output_data;
+
+    /* Convert clipboard contents */
+    guac_iconv(GUAC_READ_UTF8_NORMALIZED, &data, length,
+            GUAC_WRITE_UTF8, &output, GUAC_COMMON_CLIPBOARD_MAX_LENGTH);
+
+    guac_common_clipboard_append(terminal->clipboard, output_data, output - output_data);
 }
 
 void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) {
diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h
index db85b2a..1d74621 100644
--- a/src/terminal/terminal/display.h
+++ b/src/terminal/terminal/display.h
@@ -45,6 +45,17 @@
 #define GUAC_TERMINAL_MAX_CHAR_WIDTH 2
 
 /**
+ * The size of margins between the console text and the border in mm.
+ */
+#define GUAC_TERMINAL_MARGINS 2
+
+/**
+ * 1 inch is 25.4 millimeters, and we can therefore use the following
+ * to create a mm to px formula: (mm × dpi) ÷ 25.4 = px.
+ */
+#define GUAC_TERMINAL_MM_PER_INCH 25.4
+
+/**
  * All available terminal operations which affect character cells.
  */
 typedef enum guac_terminal_operation_type {
@@ -124,6 +135,11 @@
     int height;
 
     /**
+     * The size of margins between the console text and the border in pixels.
+     */
+    int margin;
+
+    /**
      * The description of the font to use for rendering.
      */
     PangoFontDescription* font_desc;