From 929b91c10e7b1ce1c6c2634e9acfcce20844e9a1 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 29 Jul 2025 13:36:14 +0200 Subject: [PATCH 1/9] try our own tile snapping --- src/imagedisplay.c | 7 +++++++ src/tilecache.c | 36 ++++++++++++++++++++++++++++++------ src/tilecache.h | 1 + 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index 0959f13..7b3ab00 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -666,9 +666,16 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) paint.size.width = imagedisplay->paint_rect.width; paint.size.height = imagedisplay->paint_rect.height; + /* Used for tile snapping if there's no gtk snapshot system. + */ + GtkNative *native = gtk_widget_get_native(widget); + GdkSurface *surface = gtk_native_get_surface(native); + double scale_factor = gdk_surface_get_scale_factor(surface); + if (imagedisplay->tilecache && imagedisplay->tilecache->n_levels > 0) tilecache_snapshot(imagedisplay->tilecache, snapshot, + scale_factor, imagedisplay->scale, imagedisplay->x, imagedisplay->y, &paint, imagedisplay->debug); diff --git a/src/tilecache.c b/src/tilecache.c index 23b3111..da90d04 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -835,15 +835,41 @@ tilecache_draw_bounds(GtkSnapshot *snapshot, } } +#ifndef HAVE_GTK_SNAPSHOT_SET_SNAP +/* Snap a graphene rect to a hardware pixel boundary on the output surface. We + * need to do this if the gtk snap mechachanism is missing or we'll get thin + * white lines on tile edges. + */ +static void +tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double scale) +{ + double left = rint(bounds->origin.x * scale) / scale; + double top = rint(bounds->origin.y * scale) / scale; + double right = + rint((bounds->origin.x + bounds->size.width) * scale) / scale; + double bottom = + rint((bounds->origin.y + bounds->size.height) * scale) / scale; + + bounds->origin.x = left; + bounds->origin.y = top; + bounds->size.width = right - left; + bounds->size.height = bottom - top; +} +#endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ + /* Scale is how much the level0 image has been scaled, x/y is the position of - * the top-left corner of the paint_rect area in the scaled image. + * the top-left corner of @paint in the scaled image. + * + * @pixel_scale is gdk_surface_get_scale() for the surface this snapshot will + * be rendered to. * - * paint_rect is the pixel area in gtk coordinates that we paint in the widget. + * @paint is the pixel area in gtk coordinates that we paint in the widget. * * Set debug to draw tile boundaries for debugging. */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, + double scale_factor, double scale, double x, double y, graphene_rect_t *paint, gboolean debug) { /* In debug mode, scale and offset so we can see tile clipping. @@ -947,11 +973,9 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, bounds.size.height = tile->bounds0.height * scale; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - /* Without set snap, we have to hide tile edges by expanding the - * tile. + /* Without gtk snap, we have to snap tiles edges ourselves. */ - bounds.size.width += 1; - bounds.size.height += 1; + tilecache_snap_rect_to_boundary(&bounds, scale_factor); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_append_scaled_texture(snapshot, diff --git a/src/tilecache.h b/src/tilecache.h index 3e54755..d1f7af7 100644 --- a/src/tilecache.h +++ b/src/tilecache.h @@ -110,6 +110,7 @@ Tilecache *tilecache_new(); /* Render the tiles to a snapshot. */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, + double scale_factor, double scale, double x, double y, graphene_rect_t *paint, gboolean debug); #endif /*__TILECACHE_H*/ From 61172a3da6ea033d36735f36fca573682526c24e Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 29 Jul 2025 12:52:57 +0100 Subject: [PATCH 2/9] oops, should be 1.0 / scale --- src/imagedisplay.c | 7 ++++--- src/tilecache.c | 14 +++++++------- src/tilecache.h | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/imagedisplay.c b/src/imagedisplay.c index 7b3ab00..bb83501 100644 --- a/src/imagedisplay.c +++ b/src/imagedisplay.c @@ -666,16 +666,17 @@ imagedisplay_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) paint.size.width = imagedisplay->paint_rect.width; paint.size.height = imagedisplay->paint_rect.height; - /* Used for tile snapping if there's no gtk snapshot system. + /* If there's no gtk snapping, we do our own based on the hardware pixel + * size for the surface this snapshot will be rendered to. */ GtkNative *native = gtk_widget_get_native(widget); GdkSurface *surface = gtk_native_get_surface(native); - double scale_factor = gdk_surface_get_scale_factor(surface); + double pixel_size = 1.0 / gdk_surface_get_scale_factor(surface); if (imagedisplay->tilecache && imagedisplay->tilecache->n_levels > 0) tilecache_snapshot(imagedisplay->tilecache, snapshot, - scale_factor, + pixel_size, imagedisplay->scale, imagedisplay->x, imagedisplay->y, &paint, imagedisplay->debug); diff --git a/src/tilecache.c b/src/tilecache.c index da90d04..17f713b 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -841,14 +841,14 @@ tilecache_draw_bounds(GtkSnapshot *snapshot, * white lines on tile edges. */ static void -tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double scale) +tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double pixel_size) { - double left = rint(bounds->origin.x * scale) / scale; - double top = rint(bounds->origin.y * scale) / scale; + double left = rint(bounds->origin.x * pixel_size) / pixel_size; + double top = rint(bounds->origin.y * pixel_size) / pixel_size; double right = - rint((bounds->origin.x + bounds->size.width) * scale) / scale; + rint((bounds->origin.x + bounds->size.width) * pixel_size) / pixel_size; double bottom = - rint((bounds->origin.y + bounds->size.height) * scale) / scale; + rint((bounds->origin.y + bounds->size.height) * pixel_size) / pixel_size; bounds->origin.x = left; bounds->origin.y = top; @@ -869,7 +869,7 @@ tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double scale) */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, - double scale_factor, + double pixel_size, double scale, double x, double y, graphene_rect_t *paint, gboolean debug) { /* In debug mode, scale and offset so we can see tile clipping. @@ -975,7 +975,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP /* Without gtk snap, we have to snap tiles edges ourselves. */ - tilecache_snap_rect_to_boundary(&bounds, scale_factor); + tilecache_snap_rect_to_boundary(&bounds, pixel_size); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_append_scaled_texture(snapshot, diff --git a/src/tilecache.h b/src/tilecache.h index d1f7af7..4dcef1d 100644 --- a/src/tilecache.h +++ b/src/tilecache.h @@ -110,7 +110,7 @@ Tilecache *tilecache_new(); /* Render the tiles to a snapshot. */ void tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, - double scale_factor, + double pixel_size, double scale, double x, double y, graphene_rect_t *paint, gboolean debug); #endif /*__TILECACHE_H*/ From 843347722e804dc3e3393492a24f6588776f2c1f Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 29 Jul 2025 13:11:18 +0100 Subject: [PATCH 3/9] snap backdrop as well --- src/tilecache.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index 17f713b..aaee08e 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -946,6 +946,9 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, backdrop.origin.y = 0; backdrop.size.width = TILE_SIZE; backdrop.size.height = TILE_SIZE; +#ifndef HAVE_GTK_SNAPSHOT_SET_SNAP + tilecache_snap_rect_to_boundary(&backdrop, pixel_size); +#endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_append_texture(snapshot, tilecache->background_texture, &backdrop); @@ -973,8 +976,6 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, bounds.size.height = tile->bounds0.height * scale; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - /* Without gtk snap, we have to snap tiles edges ourselves. - */ tilecache_snap_rect_to_boundary(&bounds, pixel_size); #endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ From a7f695e732c41103d7f9eb0bc64ab2e6e8f2737b Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 29 Jul 2025 15:16:57 +0100 Subject: [PATCH 4/9] better backdrop snapping --- src/tilecache.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index aaee08e..b2543f9 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -940,15 +940,15 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, /* Paint the backdrop. */ graphene_rect_t backdrop = *paint; +#ifndef HAVE_GTK_SNAPSHOT_SET_SNAP + tilecache_snap_rect_to_boundary(&backdrop, pixel_size); +#endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_push_repeat(snapshot, &backdrop, NULL); backdrop.origin.x = 0; backdrop.origin.y = 0; backdrop.size.width = TILE_SIZE; backdrop.size.height = TILE_SIZE; -#ifndef HAVE_GTK_SNAPSHOT_SET_SNAP - tilecache_snap_rect_to_boundary(&backdrop, pixel_size); -#endif /*!HAVE_GTK_SNAPSHOT_SET_SNAP*/ gtk_snapshot_append_texture(snapshot, tilecache->background_texture, &backdrop); From f9fa2c3a96562f2f57b42ad6fe39c2f5076ccc8d Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 29 Jul 2025 18:15:02 +0100 Subject: [PATCH 5/9] scale tile size with desktop scale but the checkerboard PNG still fails to render correctly --- src/tilecache.c | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index b2543f9..3cc5091 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -860,8 +860,9 @@ tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double pixel_size) /* Scale is how much the level0 image has been scaled, x/y is the position of * the top-left corner of @paint in the scaled image. * - * @pixel_scale is gdk_surface_get_scale() for the surface this snapshot will - * be rendered to. + * @pixel_size is the size of hardware pixels, so at 200% desktop scaling, + * for example, this will be 0.5. In this case, we'd need to draw tiles half + * size to get 1 image pixel == 1 display pixel. * * @paint is the pixel area in gtk coordinates that we paint in the widget. * @@ -893,12 +894,12 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, tilecache, scale, x, y); #endif /*DEBUG*/ -#ifdef DEBUG_VERBOSE +#ifdef DEBUG printf(" paint x = %g, y = %g, " "width = %g, height = %g\n", paint->origin.x, paint->origin.y, paint->size.width, paint->size.height); -#endif /*DEBUG_VERBOSE*/ +#endif /*DEBUG*/ #ifdef DEBUG_VERBOSE printf("tilecache_snapshot: %p tiles are:\n", tilecache); @@ -915,13 +916,23 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, else z = VIPS_CLIP(0, log(1.0 / scale) / log(2.0), tilecache->n_levels - 1); - /* paint_rect in level0 coordinates. + /* paint_rect in image level0 coordinates. We want 1 image pixel == 1 + * display hardware pixel, so we need to also scale by pixel_size. */ graphene_rect_t viewport; - viewport.origin.x = x / scale; - viewport.origin.y = y / scale; - viewport.size.width = VIPS_MAX(1, paint->size.width / scale); - viewport.size.height = VIPS_MAX(1, paint->size.height / scale); + viewport.origin.x = x / (scale * pixel_size); + viewport.origin.y = y / (scale * pixel_size); + viewport.size.width = + VIPS_MAX(1, paint->size.width / (scale * pixel_size)); + viewport.size.height = + VIPS_MAX(1, paint->size.height / (scale * pixel_size)); + +#ifdef DEBUG + printf(" viewport image0 coordinates x = %g, y = %g, " + "width = %g, height = %g\n", + viewport.origin.x, viewport.origin.y, + viewport.size.width, viewport.size.height); +#endif /*DEBUG*/ /* Fetch any tiles we are missing, update any tiles we have that have * been flagged as having pixels ready for fetching. @@ -964,16 +975,18 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, /* If we are zooming in beyond 1:1, we want nearest so we don't * blur the image. For zooming out, we want trilinear to get * mipmaps and antialiasing. - */ GskScalingFilter filter = scale > 1.0 ? GSK_SCALING_FILTER_NEAREST : GSK_SCALING_FILTER_TRILINEAR; + */ + GskScalingFilter filter = GSK_SCALING_FILTER_NEAREST; graphene_rect_t bounds; - - bounds.origin.x = tile->bounds0.left * scale - x + paint->origin.x; - bounds.origin.y = tile->bounds0.top * scale - y + paint->origin.y; - bounds.size.width = tile->bounds0.width * scale; - bounds.size.height = tile->bounds0.height * scale; + bounds.origin.x = + tile->bounds0.left * scale * pixel_size - x + paint->origin.x; + bounds.origin.y = + tile->bounds0.top * scale * pixel_size - y + paint->origin.y; + bounds.size.width = tile->bounds0.width * scale * pixel_size; + bounds.size.height = tile->bounds0.height * scale * pixel_size; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP tilecache_snap_rect_to_boundary(&bounds, pixel_size); From f5d198d66a9264c0b2dbacaf461428627abff4cf Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Wed, 30 Jul 2025 12:41:58 +0100 Subject: [PATCH 6/9] always use NN --- src/tilecache.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tilecache.c b/src/tilecache.c index 3cc5091..110c18d 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -978,7 +978,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, GskScalingFilter filter = scale > 1.0 ? GSK_SCALING_FILTER_NEAREST : GSK_SCALING_FILTER_TRILINEAR; */ - GskScalingFilter filter = GSK_SCALING_FILTER_NEAREST; + GskScalingFilter filter = GSK_SCALING_FILTER_TRILINEAR; graphene_rect_t bounds; bounds.origin.x = From d21d01c37fe5d23461463d92cbcf4ef535637079 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Wed, 30 Jul 2025 12:47:01 +0100 Subject: [PATCH 7/9] debug and NN --- src/tilecache.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index 110c18d..efada94 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -32,8 +32,8 @@ /* #define DEBUG_RENDER_TIME #define DEBUG_VERBOSE -#define DEBUG */ +#define DEBUG enum { /* Properties. @@ -978,7 +978,7 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, GskScalingFilter filter = scale > 1.0 ? GSK_SCALING_FILTER_NEAREST : GSK_SCALING_FILTER_TRILINEAR; */ - GskScalingFilter filter = GSK_SCALING_FILTER_TRILINEAR; + GskScalingFilter filter = GSK_SCALING_FILTER_NEAREST; graphene_rect_t bounds; bounds.origin.x = From db9031c72f32f2d8cfb9b634d04caa5e6c0ce86c Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Wed, 30 Jul 2025 12:53:35 +0100 Subject: [PATCH 8/9] Revert "scale tile size with desktop scale" This reverts commit f9fa2c3a96562f2f57b42ad6fe39c2f5076ccc8d. --- src/tilecache.c | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/tilecache.c b/src/tilecache.c index efada94..b2543f9 100644 --- a/src/tilecache.c +++ b/src/tilecache.c @@ -32,8 +32,8 @@ /* #define DEBUG_RENDER_TIME #define DEBUG_VERBOSE - */ #define DEBUG + */ enum { /* Properties. @@ -860,9 +860,8 @@ tilecache_snap_rect_to_boundary(graphene_rect_t *bounds, double pixel_size) /* Scale is how much the level0 image has been scaled, x/y is the position of * the top-left corner of @paint in the scaled image. * - * @pixel_size is the size of hardware pixels, so at 200% desktop scaling, - * for example, this will be 0.5. In this case, we'd need to draw tiles half - * size to get 1 image pixel == 1 display pixel. + * @pixel_scale is gdk_surface_get_scale() for the surface this snapshot will + * be rendered to. * * @paint is the pixel area in gtk coordinates that we paint in the widget. * @@ -894,12 +893,12 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, tilecache, scale, x, y); #endif /*DEBUG*/ -#ifdef DEBUG +#ifdef DEBUG_VERBOSE printf(" paint x = %g, y = %g, " "width = %g, height = %g\n", paint->origin.x, paint->origin.y, paint->size.width, paint->size.height); -#endif /*DEBUG*/ +#endif /*DEBUG_VERBOSE*/ #ifdef DEBUG_VERBOSE printf("tilecache_snapshot: %p tiles are:\n", tilecache); @@ -916,23 +915,13 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, else z = VIPS_CLIP(0, log(1.0 / scale) / log(2.0), tilecache->n_levels - 1); - /* paint_rect in image level0 coordinates. We want 1 image pixel == 1 - * display hardware pixel, so we need to also scale by pixel_size. + /* paint_rect in level0 coordinates. */ graphene_rect_t viewport; - viewport.origin.x = x / (scale * pixel_size); - viewport.origin.y = y / (scale * pixel_size); - viewport.size.width = - VIPS_MAX(1, paint->size.width / (scale * pixel_size)); - viewport.size.height = - VIPS_MAX(1, paint->size.height / (scale * pixel_size)); - -#ifdef DEBUG - printf(" viewport image0 coordinates x = %g, y = %g, " - "width = %g, height = %g\n", - viewport.origin.x, viewport.origin.y, - viewport.size.width, viewport.size.height); -#endif /*DEBUG*/ + viewport.origin.x = x / scale; + viewport.origin.y = y / scale; + viewport.size.width = VIPS_MAX(1, paint->size.width / scale); + viewport.size.height = VIPS_MAX(1, paint->size.height / scale); /* Fetch any tiles we are missing, update any tiles we have that have * been flagged as having pixels ready for fetching. @@ -975,18 +964,16 @@ tilecache_snapshot(Tilecache *tilecache, GtkSnapshot *snapshot, /* If we are zooming in beyond 1:1, we want nearest so we don't * blur the image. For zooming out, we want trilinear to get * mipmaps and antialiasing. + */ GskScalingFilter filter = scale > 1.0 ? GSK_SCALING_FILTER_NEAREST : GSK_SCALING_FILTER_TRILINEAR; - */ - GskScalingFilter filter = GSK_SCALING_FILTER_NEAREST; graphene_rect_t bounds; - bounds.origin.x = - tile->bounds0.left * scale * pixel_size - x + paint->origin.x; - bounds.origin.y = - tile->bounds0.top * scale * pixel_size - y + paint->origin.y; - bounds.size.width = tile->bounds0.width * scale * pixel_size; - bounds.size.height = tile->bounds0.height * scale * pixel_size; + + bounds.origin.x = tile->bounds0.left * scale - x + paint->origin.x; + bounds.origin.y = tile->bounds0.top * scale - y + paint->origin.y; + bounds.size.width = tile->bounds0.width * scale; + bounds.size.height = tile->bounds0.height * scale; #ifndef HAVE_GTK_SNAPSHOT_SET_SNAP tilecache_snap_rect_to_boundary(&bounds, pixel_size); From a46efe54c51c3d6a1bf9b772de2222931b33c646 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Wed, 30 Jul 2025 13:05:15 +0100 Subject: [PATCH 9/9] note high-dpi issues --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b5d3479..0d9a24b 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,15 @@ Clone and run: ## TODO +- add high-DPI support + + for 2900% desktop scale (for example) we probably need to render our + 256x256 tiles as 128x128 gtk pixels + + however, this seems to cause filtering problems, mysteriously, see + https://github.com/jcupitt/vipsdisp/tree/add-high-dpi for a quick test + hack + - skip unknown files on next / prev? - need to add the test to next-image, not glob, since we can't test the