Subject: bug#68006: 30.0.50; Image-mode speed

From: Manuel Giraud <[email-masked]>
Date: Mon, 21 Oct 2024 16:25:22 +0200

Eli Zaretskii <[email-masked]> writes:

>> From: Manuel Giraud <[email-masked]>
>> Cc: [email-masked],  [email-masked]
>> Date: Mon, 21 Oct 2024 12:12:12 +0200
>> 
>> Eli Zaretskii <[email-masked]> writes:
>> 
>> [...]
>> 
>> >> For the moment, I wanted to keep it minimal so the only interface to use
>> >> it goes as follow.  When a user (or a program) use the image attribute
>> >> ":ttl TIME_IN_SECONDS" with `create-image' such image will be stored in
>> >> the « user cache » and not in the internal one.  Images lookups use both
>> >> caches (internal and internal, in that order).  A negative
>> >> TIME_IN_SECONDS means « cache this image forever ».
>> >
>> > Why do we need a separate cache for this? couldn't we instead just add
>> > the EOL field to the single cache we already have?  having two caches
>> > requires to search both of them, which slows down image look up, so if
>> > we can avoid that, it's better.
>> 
>> That's you (in this thread) that suggested having another cache for more
>> "user controlled" usage and suggested that lookup_image could search in
>> both caches.
>
> I said that assuming the difference will be more prominent than just
> one field.
>
> The implementation could be a single cache with a field or fields that
> distinguish between the two kinds of cached images.

Yes, that makes sense.

>> >> I think there will be a need for user function that completely flushes
>> >> the images in this new cache.  But do you think we need others commands
>> >> for such interface?
>> >
>> > We probably need a way for modifying the time an image is to be kept,
>> > at least.
>> 
>> Yes.  Anyway, my last patch was completely wrong and I am working on
>> another one.  Tell me if you're interested to see where I'm headed to.
>
> It's your call.  I and others are here to provide feedback if you feel
> you need that.

Anyway, here is a patch of what I have done.

Since in the end an image is identify by an ID which is its index in the
images vector, I choose to take this vector out of the cache struct and
put it into what I called an image_store.

Now there are two caches (internal (as before) and user's) that both use
the image_store.  In order to make this work, I needed to add a
reference to the cache that manage it in each image.

But, as you said, for just one other field, this code might be
over-engineered and also quite error-prone.

>From 295a9501067511bf82d48f273c88b9b9f94f483f Mon Sep 17 00:00:00 2001
From: Manuel Giraud <[email-masked]>
Date: Thu, 17 Oct 2024 11:27:56 +0200
Subject: [PATCH] User controlled image cache

---
 src/alloc.c      |   3 +-
 src/dispextern.h |  31 +++--
 src/frame.h      |  16 ++-
 src/image.c      | 310 ++++++++++++++++++++++++++++++++++++-----------
 src/xfaces.c     |  19 +++
 5 files changed, 290 insertions(+), 89 deletions(-)

diff --git a/src/alloc.c b/src/alloc.c
index 4fab0d54248..eee76fa11ce 100644
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -7042,8 +7042,7 @@ mark_frame (struct Lisp_Vector *ptr)
 #ifdef HAVE_WINDOW_SYSTEM
   /* Mark this frame's image cache, though it might be common to several
      frames with the same font size.  */
-  if (FRAME_IMAGE_CACHE (f))
-    mark_image_cache (FRAME_IMAGE_CACHE (f));
+  mark_image_store (f);
 #endif /* HAVE_WINDOW_SYSTEM */
 }
 
diff --git a/src/dispextern.h b/src/dispextern.h
index cc248a4472e..63445772509 100644
--- a/src/dispextern.h
+++ b/src/dispextern.h
@@ -3141,6 +3141,12 @@ reset_mouse_highlight (Mouse_HLInfo *hlinfo)
      in prepare_image_for_display.  */
   struct timespec timestamp;
 
+  /* The end of life for an image in the user's image cache.  */
+  struct timespec eol;
+
+  /* Link to the cache used for this image.  */
+  struct image_cache *cache;
+
   /* Pixmaps of the image.  */
   Emacs_Pixmap pixmap, mask;
 
@@ -3277,23 +3283,25 @@ #define CENTERED_IMAGE_ASCENT -1
   struct image *next, *prev;
 };
 
-
-/* Cache of images.  Each frame has a cache.  X frames with the same
-   x_display_info share their caches.  */
-
-struct image_cache
+struct image_store
 {
-  /* Hash table of images.  */
-  struct image **buckets;
-
   /* Vector mapping image ids to images.  */
   struct image **images;
 
   /* Allocated size of `images'.  */
   ptrdiff_t size;
 
-  /* Number of images in the cache.  */
+  /* Number of images in the store.  */
   ptrdiff_t used;
+};
+
+/* Cache of images.  Each frame has a cache.  X frames with the same
+   x_display_info share their caches.  */
+
+struct image_cache
+{
+  /* Hash table of images.  */
+  struct image **buckets;
 
   /* Reference count (number of frames sharing this cache).  */
   ptrdiff_t refcount;
@@ -3632,6 +3640,7 @@ #define TRY_WINDOW_IGNORE_FONTS_CHANGE	(1 << 1)
 #ifdef HAVE_WINDOW_SYSTEM
 
 extern void clear_image_cache (struct frame *, Lisp_Object);
+extern void clear_user_image_cache (struct frame *);
 extern ptrdiff_t image_bitmap_pixmap (struct frame *, ptrdiff_t);
 extern void image_reference_bitmap (struct frame *, ptrdiff_t);
 extern ptrdiff_t image_create_bitmap_from_data (struct frame *, char *,
@@ -3652,10 +3661,11 @@ #define TRY_WINDOW_IGNORE_FONTS_CHANGE	(1 << 1)
 #endif
 extern Lisp_Object image_find_image_file (Lisp_Object);
 
+struct image_store *make_image_store (void);
 struct image_cache *make_image_cache (void);
 extern void free_image_cache (struct frame *);
 void clear_image_caches (Lisp_Object);
-void mark_image_cache (struct image_cache *);
+void mark_image_store (struct frame *);
 void image_prune_animation_caches (bool);
 bool valid_image_p (Lisp_Object);
 void prepare_image_for_display (struct frame *, struct image *);
@@ -3719,6 +3729,7 @@ #define RGB_PIXEL_COLOR COLORREF
 int lookup_derived_face (struct window *, struct frame *,
                          Lisp_Object, int, bool);
 #ifdef HAVE_WINDOW_SYSTEM
+extern struct image_store *share_image_store (struct frame *f);
 extern struct image_cache *share_image_cache (struct frame *f);
 #endif /* HAVE_WINDOW_SYSTEM */
 void init_frame_faces (struct frame *);
diff --git a/src/frame.h b/src/frame.h
index 1d920d1a6bc..6ded69ef14f 100644
--- a/src/frame.h
+++ b/src/frame.h
@@ -292,6 +292,12 @@ #define EMACS_FRAME_H
   /* Cache of realized images, which may be shared with other
      frames.  */
   struct image_cache *image_cache;
+
+  /* Another image cache that can be managed from Emacs. */
+  struct image_cache *user_image_cache;
+
+  /* Vector of cached images.  Used by internal and user's cache.  */
+  struct image_store *image_store;
 #endif /* HAVE_WINDOW_SYSTEM */
 
   /* Tab-bar item index of the item on which a mouse button was pressed.  */
@@ -916,6 +922,8 @@ #define FRAME_KBOARD(f) ((f)->terminal->kboard)
 
 /* Return a pointer to the image cache of frame F.  */
 #define FRAME_IMAGE_CACHE(F) ((F)->image_cache)
+#define FRAME_USER_IMAGE_CACHE(F) ((F)->user_image_cache)
+#define FRAME_IMAGE_STORE(F) ((F)->image_store)
 
 #define XFRAME(p) \
   (eassert (FRAMEP (p)), XUNTAG (p, Lisp_Vectorlike, struct frame))
@@ -1668,8 +1676,8 @@ FACE_FROM_ID_OR_NULL (struct frame *f, int id)
 INLINE struct image *
 IMAGE_FROM_ID (struct frame *f, int id)
 {
-  eassert (0 <= id && id < FRAME_IMAGE_CACHE (f)->used);
-  return FRAME_IMAGE_CACHE (f)->images[id];
+  eassert (0 <= id && id < FRAME_IMAGE_STORE (f)->used);
+  return FRAME_IMAGE_STORE (f)->images[id];
 }
 
 /* Value is a pointer to the image with id ID on frame F, or null if
@@ -1678,9 +1686,9 @@ IMAGE_FROM_ID (struct frame *f, int id)
 INLINE struct image *
 IMAGE_OPT_FROM_ID (struct frame *f, int id)
 {
-  int used = FRAME_IMAGE_CACHE (f)->used;
+  int used = FRAME_IMAGE_STORE (f)->used;
   eassume (0 <= used);
-  return 0 <= id && id < used ? FRAME_IMAGE_CACHE (f)->images[id] : NULL;
+  return 0 <= id && id < used ? FRAME_IMAGE_STORE (f)->images[id] : NULL;
 }
 #endif
 
diff --git a/src/image.c b/src/image.c
index 34936977a40..7d6ad18bd51 100644
--- a/src/image.c
+++ b/src/image.c
@@ -1744,6 +1744,7 @@ make_image (Lisp_Object spec, EMACS_UINT hash)
 {
   struct image *img = xzalloc (sizeof *img);
   Lisp_Object file = image_spec_value (spec, QCfile, NULL);
+  Lisp_Object ttl = image_spec_value (spec, QCttl, NULL);
 
   eassert (valid_image_p (spec));
   img->dependencies = NILP (file) ? Qnil : list1 (file);
@@ -1754,18 +1755,36 @@ make_image (Lisp_Object spec, EMACS_UINT hash)
   img->ascent = DEFAULT_IMAGE_ASCENT;
   img->hash = hash;
   img->corners[BOT_CORNER] = -1;  /* Full image */
+  if (! NILP (ttl) && FIXNUMP (ttl))
+    {
+      struct timespec now = current_timespec ();
+      struct timespec ttl_spec;
+
+      /* A negative TTL is used to mean forever.  */
+      if (XFIXNUM (ttl) < 0)
+	(img->eol).tv_sec = -1;
+      else
+	{
+	  ttl_spec.tv_sec = XFIXNUM (ttl);
+	  ttl_spec.tv_nsec = 0;
+	  img->eol = timespec_add(now, ttl_spec);
+	}
+    }
+  else
+    (img->eol).tv_sec = (img->eol).tv_nsec = 0;
+
   return img;
 }

-/* Free image IMG which was used on frame F, including its resources.  */
+/* Free image IMG from the cache C, including its resources on frame F.  */
 
 static void
-free_image (struct frame *f, struct image *img)
+free_image (struct image_cache *c, struct frame *f, struct image *img)
 {
   if (img)
     {
-      struct image_cache *c = FRAME_IMAGE_CACHE (f);
+      struct image_store *s = FRAME_IMAGE_STORE (f);
 
       /* Remove IMG from the hash table of its cache.  */
       if (img->prev)
@@ -1776,7 +1795,8 @@ free_image (struct frame *f, struct image *img)
       if (img->next)
 	img->next->prev = img->prev;
 
-      c->images[img->id] = NULL;
+      /* Free store's slot.  */
+      s->images[img->id] = NULL;
 
 #if !defined USE_CAIRO && defined HAVE_XRENDER
       /* FRAME_X_DISPLAY (f) could be NULL if this is being called from
@@ -2178,7 +2198,20 @@ image_alloc_image_color (struct frame *f, struct image *img,
 			     Image Cache
  ***********************************************************************/
 
+static void do_cache_image (struct image_cache *, struct frame *, struct image *);
 static void cache_image (struct frame *f, struct image *img);
+static void user_cache_image (struct frame *f, struct image *img);
+
+struct image_store *
+make_image_store (void)
+{
+  struct image_store *s = xmalloc (sizeof *s);
+
+  s->size = 50;
+  s->used = 0;
+  s->images = xmalloc (s->size * sizeof *s->images);
+  return s;
+}
 
 /* Return a new, initialized image cache that is allocated from the
    heap.  Call free_image_cache to free an image cache.  */
@@ -2188,25 +2221,22 @@ make_image_cache (void)
 {
   struct image_cache *c = xmalloc (sizeof *c);
 
-  c->size = 50;
-  c->used = c->refcount = 0;
-  c->images = xmalloc (c->size * sizeof *c->images);
+  c->refcount = 0;
   c->buckets = xzalloc (IMAGE_CACHE_BUCKETS_SIZE * sizeof *c->buckets);
   /* This value should never be encountered.  */
   c->scaling_col_width = -1;
   return c;
 }
 
-/* Find an image matching SPEC in the cache, and return it.  If no
+/* Find an image matching SPEC in the cache C, and return it.  If no
    image is found, return NULL.  */
 static struct image *
-search_image_cache (struct frame *f, Lisp_Object spec, EMACS_UINT hash,
+search_image_cache (struct image_cache *c, Lisp_Object spec, EMACS_UINT hash,
                     unsigned long foreground, unsigned long background,
                     int font_size, char *font_family, bool ignore_colors)
 {
   struct image *img;
-  struct image_cache *c = FRAME_IMAGE_CACHE (f);
-  int i = hash % IMAGE_CACHE_BUCKETS_SIZE;
+  int i;
 
   if (!c) return NULL;
 
@@ -2222,6 +2252,7 @@ search_image_cache (struct frame *f, Lisp_Object spec, EMACS_UINT hash,
      image spec specifies :background.  However, the extra memory
      usage is probably negligible in practice, so we don't bother.  */
 
+  i = hash % IMAGE_CACHE_BUCKETS_SIZE;
   for (img = c->buckets[i]; img; img = img->next)
     if (img->hash == hash
 	&& !NILP (Fequal (img->spec, spec))
@@ -2258,11 +2289,13 @@ filter_image_spec (Lisp_Object spec)
 	  spec = XCDR (spec);
 
 	  /* Some animation-related data doesn't affect display, but
-	     breaks the image cache.  Filter those out.  */
+	     breaks the image cache.  Filter those out.  Also filter out
+	     the user's cache time to live.  */
 	  if (!(EQ (key, QCanimate_buffer)
 		|| EQ (key, QCanimate_tardiness)
 		|| EQ (key, QCanimate_position)
-		|| EQ (key, QCanimate_multi_frame_data)))
+		|| EQ (key, QCanimate_multi_frame_data)
+		|| EQ (key, QCttl)))
 	    {
 	      out = Fcons (value, out);
 	      out = Fcons (key, out);
@@ -2279,14 +2312,15 @@ uncache_image (struct frame *f, Lisp_Object spec)
 {
   struct image *img;
   EMACS_UINT hash = sxhash (filter_image_spec (spec));
+  struct image_cache *c = FRAME_IMAGE_CACHE (f);
 
   /* Because the background colors are based on the current face, we
      can have multiple copies of an image with the same spec. We want
      to remove them all to ensure the user doesn't see an old version
      of the image when the face changes.  */
-  while ((img = search_image_cache (f, spec, hash, 0, 0, 0, NULL, true)))
+  while ((img = search_image_cache (c, spec, hash, 0, 0, 0, NULL, true)))
     {
-      free_image (f, img);
+      free_image (c, f, img);
       /* As display glyphs may still be referring to the image ID, we
 	 must garbage the frame (Bug#6426).  */
       SET_FRAME_GARBAGED (f);
@@ -2301,6 +2335,7 @@ uncache_image (struct frame *f, Lisp_Object spec)
 free_image_cache (struct frame *f)
 {
   struct image_cache *c = FRAME_IMAGE_CACHE (f);
+  struct image_store *s = FRAME_IMAGE_STORE (f);
   ptrdiff_t i;
 
   /* This function assumes the caller already verified that the frame's
@@ -2309,9 +2344,8 @@ free_image_cache (struct frame *f)
   /* Cache should not be referenced by any frame when freed.  */
   eassert (c->refcount == 0);
 
-  for (i = 0; i < c->used; ++i)
-    free_image (f, c->images[i]);
-  xfree (c->images);
+  for (i = 0; i < s->used; ++i)
+      free_image (c, f, s->images[i]);
   xfree (c->buckets);
   xfree (c);
 }
@@ -2328,8 +2362,9 @@ free_image_cache (struct frame *f)
 clear_image_cache (struct frame *f, Lisp_Object filter)
 {
   struct image_cache *c = FRAME_IMAGE_CACHE (f);
+  struct image_store *s = FRAME_IMAGE_STORE (f);
 
-  if (c && !f->inhibit_clear_image_cache)
+  if (c && s && !f->inhibit_clear_image_cache)
     {
       ptrdiff_t i, nfreed = 0;
 
@@ -2340,13 +2375,15 @@ clear_image_cache (struct frame *f, Lisp_Object filter)
       if (!NILP (filter))
 	{
 	  /* Filter image cache.  */
-	  for (i = 0; i < c->used; ++i)
+	  for (i = 0; i < s->used; ++i)
 	    {
-	      struct image *img = c->images[i];
-	      if (img && (EQ (Qt, filter)
-			  || !NILP (Fmember (filter, img->dependencies))))
+	      struct image *img = s->images[i];
+	      if (img && (img->cache == c)
+		  && (EQ (Qt, filter)
+		      || !NILP (Fmember (filter, img->dependencies))))
 		{
-		  free_image (f, img);
+		  free_image (c, f, img);
+		  s->images[i] = NULL;
 		  ++nfreed;
 		}
 	    }
@@ -2358,8 +2395,8 @@ clear_image_cache (struct frame *f, Lisp_Object filter)
 	  double delay;
 	  ptrdiff_t nimages = 0;
 
-	  for (i = 0; i < c->used; ++i)
-	    if (c->images[i])
+	  for (i = 0; i < s->used; ++i)
+	    if (s->images[i] && s->images[i]->cache == c)
 	      nimages++;
 
 	  /* If the number of cached images has grown unusually large,
@@ -2372,12 +2409,13 @@ clear_image_cache (struct frame *f, Lisp_Object filter)
 	  t = current_timespec ();
 	  old = timespec_sub (t, dtotimespec (delay));
 
-	  for (i = 0; i < c->used; ++i)
+	  for (i = 0; i < s->used; ++i)
 	    {
-	      struct image *img = c->images[i];
-	      if (img && timespec_cmp (img->timestamp, old) < 0)
+	      struct image *img = s->images[i];
+	      if (img && img->cache == c
+		  && timespec_cmp (img->timestamp, old) < 0)
 		{
-		  free_image (f, img);
+		  free_image (c, f, img);
 		  ++nfreed;
 		}
 	    }
@@ -2405,6 +2443,57 @@ clear_image_cache (struct frame *f, Lisp_Object filter)
     }
 }
 
+void
+clear_user_image_cache (struct frame *f)
+{
+  struct image_cache *c = FRAME_USER_IMAGE_CACHE (f);
+  struct image_store *s = FRAME_IMAGE_STORE (f);
+
+  if (c && s && !f->inhibit_clear_image_cache)
+    {
+      ptrdiff_t i, nfreed = 0;
+      struct timespec t = current_timespec ();
+
+      /* Block input so that we won't be interrupted by a SIGIO
+	 while being in an inconsistent state.  */
+      block_input ();
+
+      /* Filter image cache based on image's time to live.  */
+      for (i = 0; i < s->used; ++i)
+	{
+	  struct image *img = s->images[i];
+	  if (img && img->cache == c)
+	    {
+	      if (((img->eol).tv_sec >= 0) && timespec_cmp (t, img->eol) > 0)
+		{
+		  free_image (c, f, img);
+		  ++nfreed;
+		}
+	    }
+	}
+
+      /* We may be clearing the image cache because, for example,
+	 Emacs was iconified for a longer period of time.  In that
+	 case, current matrices may still contain references to
+	 images freed above.  So, clear these matrices.  */
+      if (nfreed)
+	{
+	  Lisp_Object tail, frame;
+
+	  FOR_EACH_FRAME (tail, frame)
+	    {
+	      struct frame *fr = XFRAME (frame);
+	      if (FRAME_USER_IMAGE_CACHE (fr) == c)
+		clear_current_matrices (fr);
+	    }
+
+	  windows_or_buffers_changed = 19;
+	}
+
+      unblock_input ();
+    }
+}
+
 void
 clear_image_caches (Lisp_Object filter)
 {
@@ -2415,7 +2504,10 @@ clear_image_caches (Lisp_Object filter)
   Lisp_Object tail, frame;
   FOR_EACH_FRAME (tail, frame)
     if (FRAME_WINDOW_P (XFRAME (frame)))
-      clear_image_cache (XFRAME (frame), filter);
+      {
+	clear_image_cache (XFRAME (frame), filter);
+	clear_user_image_cache (XFRAME (frame));
+      }
 }
 
 DEFUN ("clear-image-cache", Fclear_image_cache, Sclear_image_cache,
@@ -2444,7 +2536,11 @@ DEFUN ("clear-image-cache", Fclear_image_cache, Sclear_image_cache,
   if (! (NILP (filter) || FRAMEP (filter)))
     clear_image_caches (filter);
   else
-    clear_image_cache (decode_window_system_frame (filter), Qt);
+    {
+      struct frame *decoded_frame = decode_window_system_frame (filter);
+      clear_image_cache (decoded_frame, Qt);
+      clear_user_image_cache (decoded_frame);
+    }
 
   /* Also clear the animation caches.  */
   image_prune_animation_caches (true);
@@ -2501,17 +2597,18 @@ image_size_in_bytes (struct image *img)
 }
 
 static size_t
-image_frame_cache_size (struct frame *f)
+image_frame_cache_size (struct image_cache *c, struct frame *f)
 {
-  struct image_cache *c = FRAME_IMAGE_CACHE (f);
   if (!c)
     return 0;
 
+  struct image_store *s = FRAME_IMAGE_STORE (f);
   size_t total = 0;
-  for (ptrdiff_t i = 0; i < c->used; ++i)
+  for (ptrdiff_t i = 0; i < s->used; ++i)
     {
-      struct image *img = c->images[i];
-      total += img ? image_size_in_bytes (img) : 0;
+      struct image *img = s->images[i];
+      if (img && img->cache == c)
+	total += image_size_in_bytes (img);
     }
   return total;
 }
@@ -3495,6 +3592,8 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id)
   unsigned long background = face->background;
   int font_size = face->font->pixel_size;
   char *font_family = SSDATA (face->lface[LFACE_FAMILY_INDEX]);
+  struct image_cache *c;
+  Lisp_Object ttl = image_spec_value (spec, QCttl, NULL);
 
   /* F must be a window-system frame, and SPEC must be a valid image
      specification.  */
@@ -3503,11 +3602,17 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id)
 
   /* Look up SPEC in the hash table of the image cache.  */
   hash = sxhash (filter_image_spec (spec));
-  img = search_image_cache (f, spec, hash, foreground, background,
+  if (NILP (ttl))
+    c = FRAME_IMAGE_CACHE (f);
+  else /* SPEC as a ttl so lookup into the user's cache.  */
+    c = FRAME_USER_IMAGE_CACHE (f);
+
+  img = search_image_cache (c, spec, hash, foreground, background,
 			    font_size, font_family, false);
+
   if (img && img->load_failed_p)
     {
-      free_image (f, img);
+      free_image (c, f, img);
       img = NULL;
     }
 
@@ -3516,7 +3621,12 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id)
     {
       block_input ();
       img = make_image (spec, hash);
-      cache_image (f, img);
+      /* If image's end of life is set store it in the user's image
+	 cache instead.  */
+      if ((img->eol).tv_sec != 0)
+	user_cache_image (f, img);
+      else
+	cache_image (f, img);
       img->face_foreground = foreground;
       img->face_background = background;
       img->face_font_size = font_size;
@@ -3608,6 +3718,37 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id)
   return img->id;
 }
 
+void
+do_cache_image (struct image_cache *c, struct frame *f,
+		struct image *img)
+{
+  struct image_store *s = FRAME_IMAGE_STORE (f);
+  ptrdiff_t i;
+
+  /* Find a free slot in the store.  */
+  for (i = 0; i < s->used; ++i)
+    if (s->images[i] == NULL)
+      break;
+
+  /* If no free slot found, maybe enlarge s->images.  */
+  if (i == s->used && s->used == s->size)
+    s->images = xpalloc (s->images, &s->size, 1, -1, sizeof *s->images);
+
+  /* Add IMG to s->images, and assign IMG an id.  */
+  s->images[i] = img;
+  img->id = i;
+  if (i == s->used)
+    ++s->used;
+
+  /* Add IMG to the cache's hash table.  */
+  i = img->hash % IMAGE_CACHE_BUCKETS_SIZE;
+  img->next = c->buckets[i];
+  if (img->next)
+    img->next->prev = img;
+  img->prev = NULL;
+  c->buckets[i] = img;
+  img->cache = c;
+}
 
 /* Cache image IMG in the image cache of frame F.  */
 
@@ -3615,7 +3756,9 @@ lookup_image (struct frame *f, Lisp_Object spec, int face_id)
 cache_image (struct frame *f, struct image *img)
 {
   struct image_cache *c = FRAME_IMAGE_CACHE (f);
-  ptrdiff_t i;
+
+  if (!FRAME_IMAGE_STORE (f))
+    FRAME_IMAGE_STORE (f) = share_image_store (f);
 
   if (!c)
     {
@@ -3623,28 +3766,24 @@ cache_image (struct frame *f, struct image *img)
       c->refcount++;
     }
 
-  /* Find a free slot in c->images.  */
-  for (i = 0; i < c->used; ++i)
-    if (c->images[i] == NULL)
-      break;
+  do_cache_image (c, f, img);
+}
 
-  /* If no free slot found, maybe enlarge c->images.  */
-  if (i == c->used && c->used == c->size)
-    c->images = xpalloc (c->images, &c->size, 1, -1, sizeof *c->images);
+static void
+user_cache_image (struct frame *f, struct image *img)
+{
+  struct image_cache *c = FRAME_USER_IMAGE_CACHE (f);
 
-  /* Add IMG to c->images, and assign IMG an id.  */
-  c->images[i] = img;
-  img->id = i;
-  if (i == c->used)
-    ++c->used;
+  if (!FRAME_IMAGE_STORE (f))
+    FRAME_IMAGE_STORE (f) = share_image_store (f);
 
-  /* Add IMG to the cache's hash table.  */
-  i = img->hash % IMAGE_CACHE_BUCKETS_SIZE;
-  img->next = c->buckets[i];
-  if (img->next)
-    img->next->prev = img;
-  img->prev = NULL;
-  c->buckets[i] = img;
+  if (!c)
+    {
+      c = FRAME_USER_IMAGE_CACHE (f) = make_image_cache ();
+      c->refcount++;
+    }
+
+  do_cache_image (c, f, img);
 }

@@ -3762,14 +3901,16 @@ mark_image (struct image *img)

 void
-mark_image_cache (struct image_cache *c)
+mark_image_store (struct frame *f)
 {
-  if (c)
+  struct image_store *s = FRAME_IMAGE_STORE (f);
+
+  if (s)
     {
       ptrdiff_t i;
-      for (i = 0; i < c->used; ++i)
-	if (c->images[i])
-	  mark_image (c->images[i]);
+      for (i = 0; i < s->used; ++i)
+	if (s->images[i])
+	  mark_image (s->images[i]);
     }
 
 #if defined HAVE_WEBP || defined HAVE_GIF
@@ -12647,23 +12788,23 @@ gs_load (struct frame *f, struct image *img)
 void
 x_kill_gs_process (Pixmap pixmap, struct frame *f)
 {
-  struct image_cache *c = FRAME_IMAGE_CACHE (f);
+  struct image_store *s = FRAME_IMAGE_STORE (f);
   ptrdiff_t i;
   struct image *img;
 
   /* Find the image containing PIXMAP.  */
-  for (i = 0; i < c->used; ++i)
-    if (c->images[i]->pixmap == pixmap)
+  for (i = 0; i < s->used; ++i)
+    if (s->images[i]->pixmap == pixmap)
       break;
 
   /* Should someone in between have cleared the image cache, for
      instance, give up.  */
-  if (i == c->used)
+  if (i == s->used)
     return;
 
   /* Kill the GS process.  We should have found PIXMAP in the image
      cache and its image should contain a process object.  */
-  img = c->images[i];
+  img = s->images[i];
   eassert (PROCESSP (img->lisp_data));
   Fkill_process (img->lisp_data, Qnil);
   img->lisp_data = Qnil;
@@ -12793,11 +12934,14 @@ DEFUN ("image-cache-size", Fimage_cache_size, Simage_cache_size, 0, 0, 0,
 {
   Lisp_Object tail, frame;
   size_t total = 0;
+  struct frame *f;
 
   FOR_EACH_FRAME (tail, frame)
-    if (FRAME_WINDOW_P (XFRAME (frame)))
-      total += image_frame_cache_size (XFRAME (frame));
-
+    {
+      f = XFRAME (frame);
+      if (FRAME_WINDOW_P (f))
+	total += image_frame_cache_size (FRAME_IMAGE_CACHE (f), f);
+    }
 #if defined (HAVE_WEBP) || defined (HAVE_GIF)
   struct anim_cache *pcache = anim_cache;
   while (pcache)
@@ -12810,6 +12954,24 @@ DEFUN ("image-cache-size", Fimage_cache_size, Simage_cache_size, 0, 0, 0,
   return make_int (total);
 }
 
+DEFUN ("user-image-cache-size", Fuser_image_cache_size, Suser_image_cache_size, 0, 0, 0,
+       doc: /* Return the size of the user's image cache.  */)
+  (void)
+{
+  Lisp_Object tail, frame;
+  size_t total = 0;
+  struct frame *f;
+
+  FOR_EACH_FRAME (tail, frame)
+    {
+      f = XFRAME (frame);
+      if (FRAME_WINDOW_P (f))
+	total += image_frame_cache_size (FRAME_USER_IMAGE_CACHE (f), f);
+    }
+
+  return make_int (total);
+}
+
 
 DEFUN ("init-image-library", Finit_image_library, Sinit_image_library, 1, 1, 0,
        doc: /* Initialize image library implementing image type TYPE.
@@ -12979,6 +13141,7 @@ syms_of_image (void)
   DEFSYM (QCcolor_adjustment, ":color-adjustment");
   DEFSYM (QCmask, ":mask");
   DEFSYM (QCflip, ":flip");
+  DEFSYM (QCttl, ":ttl");
 
   /* Other symbols.  */
   DEFSYM (Qlaplace, "laplace");
@@ -13142,6 +13305,7 @@ syms_of_image (void)
   defsubr (&Simage_mask_p);
   defsubr (&Simage_metadata);
   defsubr (&Simage_cache_size);
+  defsubr (&Suser_image_cache_size);
   defsubr (&Simagep);
 
 #ifdef GLYPH_DEBUG
diff --git a/src/xfaces.c b/src/xfaces.c
index e248279e9b7..bea592fe7a8 100644
--- a/src/xfaces.c
+++ b/src/xfaces.c
@@ -636,6 +636,25 @@ x_free_gc (struct frame *f, struct android_gc *gc)
 
 #ifdef HAVE_WINDOW_SYSTEM
 
+struct image_store *
+share_image_store (struct frame *f)
+{
+  Lisp_Object tail, frame;
+  struct image_store *store;
+
+  FOR_EACH_FRAME (tail, frame)
+    {
+      struct frame *x = XFRAME (frame);
+
+      if (FRAME_TERMINAL (x) == FRAME_TERMINAL (f)
+	  && FRAME_IMAGE_STORE (x))
+	return FRAME_IMAGE_STORE (x);
+    }
+
+  store = make_image_store ();
+  return store;
+}
+
 /* Find an existing image cache registered for a frame on F's display
    and with a `scaling_col_width' of F's FRAME_COLUMN_WIDTH, or, in the
    absence of an eligible image cache, allocate an image cache with the
-- 
2.46.2

-- 
Manuel Giraud