]> asedeno.scripts.mit.edu Git - PuTTY.git/commitdiff
Use a Cairo image surface as a client-side cache.
authorSimon Tatham <anakin@pobox.com>
Thu, 24 Sep 2015 13:08:25 +0000 (14:08 +0100)
committerSimon Tatham <anakin@pobox.com>
Thu, 24 Sep 2015 13:17:20 +0000 (14:17 +0100)
In any DRAWTYPE_CAIRO mode, we now do all our Cairo drawing to a Cairo
image surface which lives on the client; then we either blit directly
from that to the window (if we're in GTK3 mode, or GTK2 without
deprecated pieces of API), or else we blit from the Cairo surface to
the server-side pixmap and then from there to the actual window.

In DRAWTYPE_GDK mode, nothing much has changed: we still draw directly
to the server-side pixmap using the GDK drawing API, then blit from
there to the window. But there is one change, namely that the blit is
no longer done proactively - instead, we queue a redraw of the
affected rectangle, and wait until we're called back by the expose
handler.

The main aim of all this is to arrange that the only time we ever draw
to the real window is in response to expose/draw events. The
experimental GTK3 OS X port stopped working a week or two ago (I
presume in response to an OS update) with the symptoms that attempts
to draw on the window outside the context of a "draw" event handler
just didn't seem to work any more; this change fixes it.

In addition to that benefit, this change also has obvious performance
advantages in principle. No more expensive text rendering in response
to an expose event - just re-copy to the window from the bitmap we
already have, from wherever it's stored that's nearest.

Moreover, this seems to have fixed the significant performance problem
with X server-side fonts under GTK. I didn't expect _that_! I'd
guessed that the approach of downloading character bitmaps and
rendering them manually via Cairo was just inherently slow for some
reason. I've no real idea _why_ this change improves matters so much;
my best guess is that perhaps there's an overhead in each drawing
operation to a GDK Cairo surface, so we win by doing lots of
operations to a much faster image surface and then batching them up
into one GDK Cairo operation. But whyever it is, I'm certainly not
complaining!

(In fact, it now seems to be noticeably _faster_, at least on my usual
local X displays, to draw server-side fonts using that technique than
using the old GDK approach. I may yet decide to switch over...)

unix/gtkwin.c

index 524754f7b8f1db0bc0feda48edafa539063324c5..de0f7a56e7cde57456c913967c62bcf1f8fe9891 100644 (file)
@@ -84,8 +84,33 @@ struct gui_data {
        *restartitem;
     GtkWidget *sessionsmenu;
 #ifndef NO_BACKING_PIXMAPS
+    /*
+     * Server-side pixmap which we use to cache the terminal window's
+     * contents. When we draw text in the terminal, we draw it to this
+     * pixmap first, and then blit from there to the actual window;
+     * this way, X expose events can be handled with an absolute
+     * minimum of network traffic, by just sending a command to
+     * re-blit an appropriate rectangle from this pixmap.
+     */
     GdkPixmap *pixmap;
 #endif
+#ifdef DRAW_TEXT_CAIRO
+    /*
+     * If we're drawing using Cairo, we cache the same image on the
+     * client side in a Cairo surface.
+     *
+     * In GTK2+Cairo, this happens _as well_ as having the server-side
+     * pixmap cache above; in GTK3+Cairo, server-side pixmaps are
+     * deprecated, so we _just_ have this client-side cache. In the
+     * latter case that means we have to transmit a big wodge of
+     * bitmap data over the X connection on every expose event; but
+     * GTK3 apparently deliberately provides no way to avoid that
+     * inefficiency, and at least this way we don't _also_ have to
+     * redo any font rendering just because the window was temporarily
+     * covered.
+     */
+    cairo_surface_t *surface;
+#endif
 #if GTK_CHECK_VERSION(2,0,0)
     GtkIMContext *imc;
 #endif
@@ -515,17 +540,31 @@ gint configure_area(GtkWidget *widget, GdkEventConfigure *event, gpointer data)
        need_size = 1;
     }
 
+    {
+        int backing_w = w * inst->font_width + 2*inst->window_border;
+        int backing_h = h * inst->font_height + 2*inst->window_border;
+
 #ifndef NO_BACKING_PIXMAPS
-    if (inst->pixmap) {
-       gdk_pixmap_unref(inst->pixmap);
-       inst->pixmap = NULL;
-    }
+        if (inst->pixmap) {
+            gdk_pixmap_unref(inst->pixmap);
+            inst->pixmap = NULL;
+        }
 
-    inst->pixmap = gdk_pixmap_new(gtk_widget_get_window(widget),
-                                 (w * inst->font_width + 2*inst->window_border),
-                                 (h * inst->font_height + 2*inst->window_border), -1);
+        inst->pixmap = gdk_pixmap_new(gtk_widget_get_window(widget),
+                                      backing_w, backing_h, -1);
 #endif
 
+#ifdef DRAW_TEXT_CAIRO
+        if (inst->surface) {
+            cairo_surface_destroy(inst->surface);
+            inst->surface = NULL;
+        }
+
+        inst->surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,
+                                                   backing_w, backing_h);
+#endif
+    }
+
     draw_backing_rect(inst);
 
     if (need_size && inst->term) {
@@ -563,40 +602,21 @@ static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
 {
     struct gui_data *inst = (struct gui_data *)data;
 
-    if (inst->term) {
-        struct draw_ctx adctx, *dctx = &adctx;
+    /*
+     * GTK3 window redraw: we always expect Cairo to be enabled, so
+     * that inst->surface exists, and pixmaps to be disabled, so that
+     * inst->pixmap does not exist. Hence, we just blit from
+     * inst->surface to the window.
+     */
+    if (inst->surface) {
         GdkRectangle dirtyrect;
 
-        dctx->inst = inst;
-        dctx->uctx.type = DRAWTYPE_CAIRO;
-        dctx->uctx.u.cairo.widget = widget;
-        dctx->uctx.u.cairo.cr = cr;
-        cairo_setup_dctx(dctx);
-
         gdk_cairo_get_clip_rectangle(cr, &dirtyrect);
 
-        /*
-         * As in window.c, we clear the 'immediately' flag in the
-         * term_paint() call if the terminal has an update pending, in
-         * case we're constrained within this event to only draw on
-         * the exposed rectangle of the window. (Because if the whole
-         * of a character cell needs a redraw due to a terminal
-         * contents change, the last thing we want is to give it a
-         * _partial_ redraw here due to system-imposed clipping, and
-         * then have the next main terminal update believe it's been
-         * redrawn in full.)
-         *
-         * I don't actually know if GTK draw events will constrain us
-         * in this way, but it's best to be careful...
-         */
-        term_paint(inst->term, dctx,
-                   (dirtyrect.x - inst->window_border) / inst->font_width,
-                   (dirtyrect.y - inst->window_border) / inst->font_height,
-                   (dirtyrect.x + dirtyrect.width -
-                    inst->window_border) / inst->font_width,
-                   (dirtyrect.y + dirtyrect.height -
-                    inst->window_border) / inst->font_height,
-                   !inst->term->window_update_pending);
+        cairo_set_source_surface(cr, inst->surface, 0, 0);
+        cairo_rectangle(cr, dirtyrect.x, dirtyrect.y,
+                        dirtyrect.width, dirtyrect.height);
+        cairo_fill(cr);
     }
 
     return TRUE;
@@ -608,8 +628,8 @@ gint expose_area(GtkWidget *widget, GdkEventExpose *event, gpointer data)
 
 #ifndef NO_BACKING_PIXMAPS
     /*
-     * Pass the exposed rectangle to terminal.c, which will call us
-     * back to do the actual painting.
+     * Draw to the exposed part of the window from the server-side
+     * backing pixmap.
      */
     if (inst->pixmap) {
        gdk_draw_pixmap(gtk_widget_get_window(widget),
@@ -621,17 +641,18 @@ gint expose_area(GtkWidget *widget, GdkEventExpose *event, gpointer data)
                        event->area.width, event->area.height);
     }
 #else
-    if (inst->term) {
-        Context ctx = get_ctx(inst);
-        term_paint(inst->term, ctx,
-                   (event->area.x - inst->window_border) / inst->font_width,
-                   (event->area.y - inst->window_border) / inst->font_height,
-                   (event->area.x + event->area.width -
-                    inst->window_border) / inst->font_width,
-                   (event->area.y + event->area.height -
-                    inst->window_border) / inst->font_height,
-                   !inst->term->window_update_pending);
-        free_ctx(ctx);
+    /*
+     * Failing that, draw from the client-side Cairo surface. (We
+     * should never be compiled in a context where we have _neither_
+     * inst->surface nor inst->pixmap.)
+     */
+    if (inst->surface) {
+        cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(widget));
+        cairo_set_source_surface(cr, inst->surface, 0, 0);
+        cairo_rectangle(cr, event->area.x, event->area.y,
+                       event->area.width, event->area.height);
+        cairo_fill(cr);
+        cairo_destroy(cr);
     }
 #endif
 
@@ -3021,30 +3042,28 @@ Context get_ctx(void *frontend)
 {
     struct gui_data *inst = (struct gui_data *)frontend;
     struct draw_ctx *dctx;
-    GdkWindow *target;
 
     if (!gtk_widget_get_window(inst->area))
        return NULL;
 
-#ifndef NO_BACKING_PIXMAPS
-    target = inst->pixmap;
-#else
-    target = gtk_widget_get_window(inst->area);
-#endif
-
     dctx = snew(struct draw_ctx);
     dctx->inst = inst;
     dctx->uctx.type = inst->drawtype;
 #ifdef DRAW_TEXT_GDK
     if (dctx->uctx.type == DRAWTYPE_GDK) {
-        dctx->uctx.u.gdk.target = target;
+        /* If we're doing GDK-based drawing, then we also expect
+         * inst->pixmap to exist. */
+        dctx->uctx.u.gdk.target = inst->pixmap;
         dctx->uctx.u.gdk.gc = gdk_gc_new(gtk_widget_get_window(inst->area));
     }
 #endif
 #ifdef DRAW_TEXT_CAIRO
     if (dctx->uctx.type == DRAWTYPE_CAIRO) {
         dctx->uctx.u.cairo.widget = GTK_WIDGET(inst->area);
-        dctx->uctx.u.cairo.cr = gdk_cairo_create(target);
+        /* If we're doing Cairo drawing, we expect inst->surface to
+         * exist, and we draw to that first, regardless of whether we
+         * subsequently copy the results to inst->pixmap. */
+        dctx->uctx.u.cairo.cr = cairo_create(inst->surface);
         cairo_setup_dctx(dctx);
     }
 #endif
@@ -3071,23 +3090,31 @@ void free_ctx(Context ctx)
 
 static void draw_update(struct draw_ctx *dctx, int x, int y, int w, int h)
 {
-#ifndef NO_BACKING_PIXMAPS
-#ifdef DRAW_TEXT_GDK
-    if (dctx->uctx.type == DRAWTYPE_GDK) {
-        gdk_draw_pixmap(gtk_widget_get_window(dctx->inst->area),
-                        dctx->uctx.u.gdk.gc, dctx->inst->pixmap,
-                        x, y, x, y, w, h);
-    }
-#endif
-#ifdef DRAW_TEXT_CAIRO /* FIXME: and not GTK3 where a cairo_t is all we have */
+#if defined DRAW_TEXT_CAIRO && !defined NO_BACKING_PIXMAPS
     if (dctx->uctx.type == DRAWTYPE_CAIRO) {
-        GdkGC *gc = gdk_gc_new(gtk_widget_get_window(dctx->inst->area));
-        gdk_draw_pixmap(gtk_widget_get_window(dctx->inst->area),
-                        gc, dctx->inst->pixmap, x, y, x, y, w, h);
-        gdk_gc_unref(gc);
+        /*
+         * If inst->surface and inst->pixmap both exist, then we've
+         * just drawn new content to the former which we must copy to
+         * the latter.
+         */
+        cairo_t *cr = gdk_cairo_create(dctx->inst->pixmap);
+        cairo_set_source_surface(cr, dctx->inst->surface, 0, 0);
+        cairo_rectangle(cr, x, y, w, h);
+        cairo_fill(cr);
+        cairo_destroy(cr);
     }
 #endif
-#endif
+
+    /*
+     * Now we just queue a window redraw, which will cause
+     * inst->surface or inst->pixmap (whichever is appropriate for our
+     * compile mode) to be copied to the real window when we receive
+     * the resulting "expose" or "draw" event.
+     *
+     * Amazingly, this one API call is actually valid in all versions
+     * of GTK :-)
+     */
+    gtk_widget_queue_draw_area(dctx->inst->area, x, y, w, h);
 }
 
 static void draw_set_colour(struct draw_ctx *dctx, int col)
@@ -3285,10 +3312,11 @@ static void draw_stretch_after(struct draw_ctx *dctx, int x, int y,
 static void draw_backing_rect(struct gui_data *inst)
 {
     struct draw_ctx *dctx = get_ctx(inst);
+    int w = inst->width * inst->font_width + 2*inst->window_border;
+    int h = inst->height * inst->font_height + 2*inst->window_border;
     draw_set_colour(dctx, 258);
-    draw_rectangle(dctx, 1, 0, 0,
-                   inst->width * inst->font_width + 2*inst->window_border,
-                   inst->height * inst->font_height + 2*inst->window_border);
+    draw_rectangle(dctx, 1, 0, 0, w, h);
+    draw_update(dctx, 0, 0, w, h);
     free_ctx(dctx);
 }