Subject: [PATCH] Support hidpi fringes and images with Cairo
From: Carlos Pita <[email-masked]>
To: [email-masked]
Date: Fri, 25 Oct 2019 23:17:43 -0300
List-Id: <debbugs-submit.debbugs.gnu.org>

Here is a patch that takes a slightly different approach than my
previous one and fixes both fringe bitmaps and general image
(including "widgets": checkboxes, arrows, etc) rendering in hidpi
screens. I've attached a number of before/after screenshots.

I added two new fields `scale_x` and `scale_y` to the frame struct. I
believe going through the rif was overkilling in terms of indirections
and in terms of legibility, specially if I wanted to implement
independent scale factors for x and y in order to honour the previous
implementation. Now it's simply f->scale_x and f->scale_y. These
fields are initialized when the frame is created. They are floating
point numbers, truncation or rounding is done after scaling.

To be rigorous, these fields should belong to the terminal or to the
display_info, because they're common to all frames in the terminal
(indeed, they are closely related to resx and resy). But display_info
structs are backend specific and adding macros or inlines to frame.h
that reference them would have required including TERM_HEADER in many
many places. Also, having them at the frame level is handy since scale
factors are often, if not always, referenced in a context where a
frame is at scope.

The strategy is simple: scale fringe bitmaps and images geometries
beforehand, so as to allow geometry calculations, but postpone actual
scaling of the contents until the moment of their actual rendering to
screen. The image module has a lot of image loading functions. In
order to reduce the risk of regressions, I decided to adjust width and
height at the end of each function, immediately before returning.

Even if I've implemented this only for cairo (both with and without
gtk), adding support for x without cairo and for w32 shouldn't be
difficult at all. The ns backend simply sets both scale factors to 1,
thus disabling scaling. Other unsupported backends now should at least
carry better geometry computations, although contents won't be
actually scaled.

This doesn't support dynamic changes of dpi or scale factor. For two
or three days (and their nights) I strived to get that working. I was
able to catch xrandr resolution-change events, I was able to catch
scale-change gtk notifications, but the assumption that everything is
set just once at the beginning is firmly entrenched in the code. In
particular, rescaling fonts on the fly turned out to be an
insurmountable problem. I tried many, many things: clearing font
caches, glyph caches, matrix caches, resetting the frame font
parameter, remapping the default face (a la C-x C-+). I finally got it
working but only for the default face, I could have gone and remapped
also modeline faces, etc. I don't think such hacks are worth the
price.

In general, what people is doing to get x working with two monitors at
different dpis is to set a common gtk scale factor that is good for
the highest resolution screen (often = 2) and then scale the lowest
resolution screen down using xrandr. This is an old hack now
"officially" implemented in Ubuntu. So, if you launched  an emacs
frame in a hidpi laptop and then plugged an external monitor with the
same or lower resolution, there won't be any problem. Otherwise, if
your emacs frame started in the lowest resolution screen, then it
should be scaled up, so the only way to go is to close it and open a
frame afresh. Sorry for that.

So, all in all, this fixes hidpi problems and should also work in the
usual multi-monitor multi-dpi scenario. For a more comprehensive
solution I see no other way than writing a modern gtk backend, which
is also the way to move towards wayland.

Best regards
--
Carlos
From 479215d0ee6d05a36faa01dff2116a44cf729f30 Mon Sep 17 00:00:00 2001
From: Carlos Pita <[email-masked]>
Date: Fri, 25 Oct 2019 22:04:11 -0300
Subject: [PATCH] Support hidpi screens with cairo

Add hidpi support for fringe bitmaps and images. Although the
framework is general, this commit only implements support for
cairo.

* src/frame.h (struct frame): Add fields `scale_x' and `scale_y'.
(FRAME_LEFT_FRINGE_WIDTH, FRAME_RIGHT_FRINGE_WIDTH): Scale widths.

* src/fringe.c (draw_fringe_bitmap_1): Scale bitmap width and height.

* src/image.c (lookup_image, xbm_load, xpm_load, pbm_load)
(png_load_body, jpeg_load_body, tiff_load, gif_load)
(imagemagick_load, svg_load, gs_load): Scale images width and height.

* src/nsfns.m (Fx_create_frame): Initialize scale factors to 1.

* src/w32fns.c (Fx_create_frame): Initialize scale factors using a
base of 96dpi.

* src/w32term.c (w32_get_scale_factor): Remove function.
(w32_draw_underwave): Use new `scale_x' and `scale_y' fields.

* src/window.h (WINDOW_LEFT_FRINGE_WIDTH, WINDOW_RIGHT_FRINGE_WIDTH):
Scale widths.

* src/xfns.c (Fx_create_frame): Initialize scale factors using a base
of 96dpi.

* src/xterm.c (x_cr_draw_image): Set surface scale before drawing.
(x_get_scale_factor): Remove function.
(x_draw_underwave): Use new `scale_x' and `scale_y' fields.
---
 src/frame.h   |  7 +++++--
 src/fringe.c  |  4 ++--
 src/image.c   | 40 ++++++++++++++++++++++++++++++++++++++++
 src/nsfns.m   |  2 ++
 src/w32fns.c  |  3 +++
 src/w32term.c | 19 +------------------
 src/window.h  | 12 ++++++------
 src/xfns.c    |  3 +++
 src/xterm.c   | 25 +++----------------------
 9 files changed, 65 insertions(+), 50 deletions(-)

diff --git a/src/frame.h b/src/frame.h
index f408f12394..aa3685c213 100644
--- a/src/frame.h
+++ b/src/frame.h
@@ -636,6 +636,9 @@ #define EMACS_FRAME_H
   unsigned long background_pixel;
   unsigned long foreground_pixel;
 
+  /* Factors used to scale pixel geometries.  */
+  double scale_x, scale_y;
+
 #ifdef NS_IMPL_COCOA
   /* NSAppearance theme used on this frame.  */
   enum ns_appearance_type ns_appearance;
@@ -1415,12 +1418,12 @@ FRAME_FRINGE_COLS (struct frame *f)
 INLINE int
 FRAME_LEFT_FRINGE_WIDTH (struct frame *f)
 {
-  return frame_dimension (f->left_fringe_width);
+  return frame_dimension (f->left_fringe_width) * f->scale_x;
 }
 INLINE int
 FRAME_RIGHT_FRINGE_WIDTH (struct frame *f)
 {
-  return frame_dimension (f->right_fringe_width);
+  return frame_dimension (f->right_fringe_width) * f->scale_x;
 }
 
 /* Total width of fringes in pixels.  */
diff --git a/src/fringe.c b/src/fringe.c
index d878d929cc..807c7803c8 100644
--- a/src/fringe.c
+++ b/src/fringe.c
@@ -602,9 +602,9 @@ draw_fringe_bitmap_1 (struct window *w, struct glyph_row *row, int left_p, int o
 
   p.which = which;
   p.bits = fb->bits;
-  p.wd = fb->width;
+  p.wd = fb->width * f->scale_x;
 
-  p.h = fb->height;
+  p.h = fb->height * f->scale_y;
   p.dh = (period > 0 ? (p.y % period) : 0);
   p.h -= p.dh;
 
diff --git a/src/image.c b/src/image.c
index 08e420837a..1a7324b406 100644
--- a/src/image.c
+++ b/src/image.c
@@ -2304,6 +2304,8 @@ lookup_image (struct frame *f, Lisp_Object spec)
 	  value = image_spec_value (spec, QCheight, NULL);
 	  img->height = (FIXNUMP (value)
 			 ? XFIXNAT (value) : DEFAULT_IMAGE_HEIGHT);
+          img->width *= f->scale_x;
+          img->height *= f->scale_y;
 	}
       else
 	{
@@ -3792,6 +3794,9 @@ xbm_load (struct frame *f, struct image *img)
 	}
     }
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return success_p;
 }
 
@@ -4472,6 +4477,10 @@ xpm_load (struct frame *f, struct image *img)
   xpm_free_color_cache ();
 #endif
   SAFE_FREE ();
+
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return rc == XpmSuccess;
 }
 
@@ -4951,6 +4960,9 @@ xpm_load (struct frame *f,
 				  SSDATA (data) + SBYTES (data));
     }
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return success_p;
 }
 
@@ -6168,6 +6180,10 @@ pbm_load (struct frame *f, struct image *img)
      img->height = height; */
 
   xfree (contents);
+
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return 1;
 }
 
@@ -6810,6 +6826,9 @@ png_load_body (struct frame *f, struct image *img, struct png_load_context *c)
       image_put_x_image (f, img, mask_img, 1);
     }
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return 1;
 }
 
@@ -7383,6 +7402,10 @@ jpeg_load_body (struct frame *f, struct image *img,
   /* Put ximg into the image.  */
   image_put_x_image (f, img, ximg, 0);
   SAFE_FREE ();
+
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return 1;
 }
 
@@ -7831,6 +7854,10 @@ tiff_load (struct frame *f, struct image *img)
   image_put_x_image (f, img, ximg, 0);
 
   xfree (buf);
+
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return 1;
 }
 
@@ -8428,6 +8455,9 @@ gif_load (struct frame *f, struct image *img)
   /* Put ximg into the image.  */
   image_put_x_image (f, img, ximg, 0);
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return 1;
 }
 
@@ -9214,6 +9244,9 @@ imagemagick_load (struct frame *f, struct image *img)
                                           SBYTES (data), NULL);
     }
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return success_p;
 }
 
@@ -9538,6 +9571,9 @@ svg_load (struct frame *f, struct image *img)
 				   : SSDATA (original_filename)));
     }
 
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return success_p;
 }
 
@@ -9886,6 +9922,10 @@ gs_load (struct frame *f, struct image *img)
 			  make_fixnum (img->height),
 			  window_and_pixmap_id,
 			  pixel_colors);
+
+  img->width *= f->scale_x;
+  img->height *= f->scale_y;
+
   return PROCESSP (img->lisp_data);
 }
 
diff --git a/src/nsfns.m b/src/nsfns.m
index 184fd71678..028b0eaf13 100644
--- a/src/nsfns.m
+++ b/src/nsfns.m
@@ -1121,6 +1121,8 @@ Turn the input menu (an NSMenu) into a lisp list for tracking on lisp side.
   else
       f = make_frame (1);
 
+  f->scale_x = f->scale_y = 1;
+
   XSETFRAME (frame, f);
 
   f->terminal = dpyinfo->terminal;
diff --git a/src/w32fns.c b/src/w32fns.c
index 4ef075f715..3f827be89c 100644
--- a/src/w32fns.c
+++ b/src/w32fns.c
@@ -5866,6 +5866,9 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame,
   else
     f = make_frame (true);
 
+  f->scale_x = dpyinfo->resx / 96;
+  f->scale_y = dpyinfo->resy / 96;
+
   XSETFRAME (frame, f);
 
   parent_frame = gui_display_get_arg (dpyinfo, parameters, Qparent_frame,
diff --git a/src/w32term.c b/src/w32term.c
index 888b7368d2..1d746500ee 100644
--- a/src/w32term.c
+++ b/src/w32term.c
@@ -304,22 +304,6 @@ w32_restore_glyph_string_clip (struct glyph_string *s)
     }
 }
 
-static void
-w32_get_scale_factor(struct w32_display_info *dpyinfo, int *scale_x, int *scale_y)
-{
-  const int base_res = 96;
-
-  *scale_x = *scale_y = 1;
-
-  if (dpyinfo)
-    {
-      if (dpyinfo->resx > base_res)
-	*scale_x = floor (dpyinfo->resx / base_res);
-      if (dpyinfo->resy > base_res)
-	*scale_y = floor (dpyinfo->resy / base_res);
-    }
-}
-
 /*
    Draw a wavy line under S. The wave fills wave_height pixels from y0.
 
@@ -336,8 +320,7 @@ w32_draw_underwave (struct glyph_string *s, COLORREF color)
 {
   struct w32_display_info *dpyinfo = FRAME_DISPLAY_INFO (s->f);
 
-  int scale_x, scale_y;
-  w32_get_scale_factor (dpyinfo, &scale_x, &scale_y);
+  double scale_x = s->f->scale_x, scale_y = s->f->scale_y;
 
   int wave_height = 3 * scale_y, wave_length = 2 * scale_x, thickness = scale_y;
   int dx, dy, x0, y0, width, x1, y1, x2, y2, odd, xmax;
diff --git a/src/window.h b/src/window.h
index 71946a5695..07e24551f0 100644
--- a/src/window.h
+++ b/src/window.h
@@ -828,14 +828,14 @@ #define WINDOW_MARGINS_WIDTH(W)			\
    + WINDOW_RIGHT_MARGIN_WIDTH (W))
 
 /* Pixel-widths of fringes.  */
-#define WINDOW_LEFT_FRINGE_WIDTH(W)			\
-  (W->left_fringe_width >= 0				\
-   ? W->left_fringe_width				\
+#define WINDOW_LEFT_FRINGE_WIDTH(W)				\
+  (W->left_fringe_width >= 0					\
+   ? (W->left_fringe_width * WINDOW_XFRAME (W)->scale_x)	\
    : FRAME_LEFT_FRINGE_WIDTH (WINDOW_XFRAME (W)))
 
-#define WINDOW_RIGHT_FRINGE_WIDTH(W)			\
-  (W->right_fringe_width >= 0				\
-   ? W->right_fringe_width				\
+#define WINDOW_RIGHT_FRINGE_WIDTH(W)				\
+  (W->right_fringe_width >= 0					\
+   ? (W->right_fringe_width * WINDOW_XFRAME (W)->scale_x)	\
    : FRAME_RIGHT_FRINGE_WIDTH (WINDOW_XFRAME (W)))
 
 #define WINDOW_FRINGES_WIDTH(W)		\
diff --git a/src/xfns.c b/src/xfns.c
index 20e63a2650..3970fe3dda 100644
--- a/src/xfns.c
+++ b/src/xfns.c
@@ -3736,6 +3736,9 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame,
   else
     f = make_frame (true);
 
+  f->scale_x = dpyinfo->resx / 96;
+  f->scale_y = dpyinfo->resy / 96;
+
   parent_frame = gui_display_get_arg (dpyinfo,
                                       parms,
                                       Qparent_frame,
diff --git a/src/xterm.c b/src/xterm.c
index 05d6a214dd..c34670aba2 100644
--- a/src/xterm.c
+++ b/src/xterm.c
@@ -635,6 +635,7 @@ x_cr_draw_image (struct frame *f, GC gc, cairo_pattern_t *image,
 
   cairo_surface_t *surface;
   cairo_pattern_get_surface (image, &surface);
+  cairo_surface_set_device_scale (surface, 1. / f->scale_x, 1. / f->scale_y);
   cairo_format_t format = cairo_image_surface_get_format (surface);
   if (format != CAIRO_FORMAT_A8 && format != CAIRO_FORMAT_A1)
     {
@@ -3663,23 +3664,6 @@ x_draw_stretch_glyph_string (struct glyph_string *s)
   s->background_filled_p = true;
 }
 
-static void
-x_get_scale_factor(Display *disp, int *scale_x, int *scale_y)
-{
-  const int base_res = 96;
-  struct x_display_info * dpyinfo = x_display_info_for_display (disp);
-
-  *scale_x = *scale_y = 1;
-
-  if (dpyinfo)
-    {
-      if (dpyinfo->resx > base_res)
-	*scale_x = floor (dpyinfo->resx / base_res);
-      if (dpyinfo->resy > base_res)
-	*scale_y = floor (dpyinfo->resy / base_res);
-    }
-}
-
 /*
    Draw a wavy line under S. The wave fills wave_height pixels from y0.
 
@@ -3693,12 +3677,8 @@ x_get_scale_factor(Display *disp, int *scale_x, int *scale_y)
 static void
 x_draw_underwave (struct glyph_string *s)
 {
-  Display *display = FRAME_X_DISPLAY (s->f);
-
   /* Adjust for scale/HiDPI.  */
-  int scale_x, scale_y;
-
-  x_get_scale_factor (display, &scale_x, &scale_y);
+  double scale_x = s->f->scale_x, scale_y = s->f->scale_y;
 
   int wave_height = 3 * scale_y, wave_length = 2 * scale_x;
 
@@ -3709,6 +3689,7 @@ x_draw_underwave (struct glyph_string *s)
   int dx, dy, x0, y0, width, x1, y1, x2, y2, xmax, thickness = scale_y;;
   bool odd;
   XRectangle wave_clip, string_clip, final_clip;
+  Display *display = FRAME_X_DISPLAY (s->f);
 
   dx = wave_length;
   dy = wave_height - 1;
-- 
2.20.1