diff --git a/src/simutrans/display/simgraph16.cc b/src/simutrans/display/simgraph16.cc
index 73600449a..cda6bade7 100644
--- a/src/simutrans/display/simgraph16.cc
+++ b/src/simutrans/display/simgraph16.cc
@@ -345,6 +345,14 @@ struct imd {
 	sint16 base_h; // height
 
 	PIXVAL* base_data; // original image data
+
+	// two-slot scaled-image cache: keep the previously used zoom level so that
+	// returning to it (rapid zoom / pinch) reuses the buffer instead of rescaling.
+	PIXVAL* zoom_data2; // secondary (LRU) scaled buffer; NULL if empty
+	sint16 x2, y2, w2, h2;
+	uint32 len2;
+	uint8 zoom_tag;  // zoom_factor cached in zoom_data  (0xFF = none)
+	uint8 zoom_tag2; // zoom_factor cached in zoom_data2 (0xFF = none)
 };
 
 // Flags for recoding
@@ -1338,15 +1346,45 @@ static void rezoom_img(const image_id n)
 			return;
 		}
 #endif
+		// two-slot cache: a hit on the secondary slot swaps it into the active slot
+		// with no rescale. The buffer is the exact output rezoom_img produced earlier,
+		// so a hit is byte-identical to a fresh rescale.
+		if(  images[n].zoom_data2 != NULL  &&  images[n].zoom_tag2 == (uint8)zoom_factor  ) {
+			PIXVAL *sd = images[n].zoom_data;  images[n].zoom_data = images[n].zoom_data2;  images[n].zoom_data2 = sd;
+			sint16 s16;
+			s16 = images[n].x; images[n].x = images[n].x2; images[n].x2 = s16;
+			s16 = images[n].y; images[n].y = images[n].y2; images[n].y2 = s16;
+			s16 = images[n].w; images[n].w = images[n].w2; images[n].w2 = s16;
+			s16 = images[n].h; images[n].h = images[n].h2; images[n].h2 = s16;
+			uint32 s32 = images[n].len; images[n].len = images[n].len2; images[n].len2 = s32;
+			uint8 s8 = images[n].zoom_tag; images[n].zoom_tag = images[n].zoom_tag2; images[n].zoom_tag2 = s8;
+			// data[] belong to the old active zoom -> drop, recode from the swapped-in buffer
+			for(  uint8 i = 0;  i < MAX_PLAYER_COUNT;  i++  ) {
+				if(  images[n].data[i] != NULL  ) { free( images[n].data[i] );  images[n].data[i] = NULL; }
+			}
+			images[n].player_flags = 0xFFFF;
+			images[n].recode_flags &= ~FLAG_REZOOM;
+#ifdef MULTI_THREAD
+			pthread_mutex_unlock( &rezoom_img_mutex[n % env_t::num_threads] );
+#endif
+			return;
+		}
+
 		// we may need night conversion afterwards
 		images[n].player_flags = 0xFFFF; // recode all player colors
 
 		//  we recalculate the len (since it may be larger than before)
 		// thus we have to free the old caches
 		if(  images[n].zoom_data != NULL  ) {
-			free( images[n].zoom_data );
+			// miss: stash the current buffer into the secondary slot (evict old LRU)
+			if(  images[n].zoom_data2 != NULL  ) { free( images[n].zoom_data2 ); }
+			images[n].zoom_data2 = images[n].zoom_data;
+			images[n].x2 = images[n].x;  images[n].y2 = images[n].y;
+			images[n].w2 = images[n].w;  images[n].h2 = images[n].h;
+			images[n].len2 = images[n].len;  images[n].zoom_tag2 = images[n].zoom_tag;
 			images[n].zoom_data = NULL;
 		}
+		images[n].zoom_tag = (uint8)zoom_factor;
 		for(  uint8 i = 0;  i < MAX_PLAYER_COUNT;  i++  ) {
 			if(  images[n].data[i] != NULL  ) {
 				free( images[n].data[i] );
@@ -2049,6 +2087,9 @@ static image_id simgraph16_register_image(const image_t *image_in)
 	}
 
 	image->zoom_data = NULL;
+	image->zoom_data2 = NULL;
+	image->zoom_tag = image->zoom_tag2 = 0xFF;
+	image->len2 = 0;
 	image->len = image_in->len;
 
 	image->base_x = image_in->x;
@@ -2072,6 +2113,9 @@ static void simgraph16_free_all_images_above( image_id above )
 		if(  images[anz_images].zoom_data != NULL  ) {
 			free( images[anz_images].zoom_data );
 		}
+		if(  images[anz_images].zoom_data2 != NULL  ) {
+			free( images[anz_images].zoom_data2 );
+		}
 		for(  uint8 i = 0;  i < MAX_PLAYER_COUNT;  i++  ) {
 			if(  images[anz_images].data[i] != NULL  ) {
 				free( images[anz_images].data[i] );
