/* Copyright  (C) 2010-2019 The RetroArch team
 *
 * ---------------------------------------------------------------------------------------
 * The following license statement only applies to this file (gfx_thumbnail.c).
 * ---------------------------------------------------------------------------------------
 *
 * Permission is hereby granted, free of charge,
 * to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <features/features_cpu.h>
#include <string/stdstring.h>
#include <file/file_path.h>
#include <lists/file_list.h>

#include "gfx_display.h"
#include "gfx_animation.h"
#include "gfx_thumbnail.h"

#include "../configuration.h"
#include "../msg_hash.h"
#include "../paths.h"
#include "../file_path_special.h"

#ifdef HAVE_MENU
#include "../menu/menu_driver.h"
#endif

#include "../tasks/tasks_internal.h"

#define DEFAULT_GFX_THUMBNAIL_STREAM_DELAY  16.66667f * 3
#define DEFAULT_GFX_THUMBNAIL_FADE_DURATION 166.66667f

/* The thumbnail .status field is atomically-typed (see the
 * gfx_thumbnail_t comment in gfx_thumbnail.h).  Reads and writes
 * therefore must go through the retro_atomic API rather than
 * plain assignment / dereference; on the C11 stdatomic and C++11
 * std::atomic backends, plain access to such a field is illegal.
 *
 * The two cross-thread sites in this file are noted at the
 * call sites:
 *  - The release-stores in gfx_thumbnail_handle_upload publish
 *    prior texture / width / height writes.
 *  - The acquire-load in gfx_thumbnail_draw pairs with those.
 * All other accesses are single-threaded; they go through the
 * retro_atomic API only because the field's atomic-typed
 * storage requires it.  The cost on weak-memory ARM/PowerPC is
 * one extra ldar/stlr per cold-path access; on x86 TSO the
 * barriers compile out entirely.
 *
 * The wrappers are static-inline (rather than function-like
 * macros) so callers have a clear function boundary.  GCC 13+
 * at -O3 emits a spurious -Wstringop-overflow on the inlined
 * __atomic_* primitive when called from gfx_thumbnail_request,
 * because the optimiser cannot prove the @c thumbnail argument
 * non-NULL across every `goto end:` flow-graph path; the
 * suppression below is a targeted fix that does not affect
 * codegen.  No effect on non-GCC backends. */

#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
#  define GFX_THUMB_STATUS_DIAG_PUSH \
   _Pragma("GCC diagnostic push") \
   _Pragma("GCC diagnostic ignored \"-Wstringop-overflow\"")
#  define GFX_THUMB_STATUS_DIAG_POP \
   _Pragma("GCC diagnostic pop")
#else
#  define GFX_THUMB_STATUS_DIAG_PUSH
#  define GFX_THUMB_STATUS_DIAG_POP
#endif

GFX_THUMB_STATUS_DIAG_PUSH
static INLINE void gfx_thumb_status_store(
      retro_atomic_int_t *ptr, enum gfx_thumbnail_status val)
{
   retro_atomic_store_release_int(ptr, (int)val);
}

static INLINE enum gfx_thumbnail_status gfx_thumb_status_load(
      retro_atomic_int_t *ptr)
{
   return (enum gfx_thumbnail_status)retro_atomic_load_acquire_int(ptr);
}
GFX_THUMB_STATUS_DIAG_POP

#define GFX_THUMB_STATUS_STORE(ptr, val) gfx_thumb_status_store((ptr), (val))
#define GFX_THUMB_STATUS_LOAD(ptr)       gfx_thumb_status_load((ptr))

/* Utility structure, sent as userdata when pushing
 * an image load */
typedef struct
{
   uint64_t list_id;
   gfx_thumbnail_t *thumbnail;
} gfx_thumbnail_tag_t;

static gfx_thumbnail_state_t gfx_thumb_st = {0}; /* uint64_t alignment */

gfx_thumbnail_state_t *gfx_thumb_get_ptr(void)
{
   return &gfx_thumb_st;
}

/* Setters */

/* When streaming thumbnails, sets time in ms that an
 * entry must be on screen before an image load is
 * requested
 * > if 'delay' is negative, default value is set */
void gfx_thumbnail_set_stream_delay(float delay)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   p_gfx_thumb->stream_delay = (delay >= 0.0f) ?
         delay : DEFAULT_GFX_THUMBNAIL_STREAM_DELAY;
}

/* Sets duration in ms of the thumbnail 'fade in'
 * animation
 * > If 'duration' is negative, default value is set */
void gfx_thumbnail_set_fade_duration(float duration)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   p_gfx_thumb->fade_duration = (duration >= 0.0f) ?
         duration : DEFAULT_GFX_THUMBNAIL_FADE_DURATION;
}

/* Specifies whether 'fade in' animation should be
 * triggered for missing thumbnails
 * > When 'true', allows menu driver to animate
 *   any 'thumbnail unavailable' notifications */
void gfx_thumbnail_set_fade_missing(bool fade_missing)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   p_gfx_thumb->fade_missing = fade_missing;
}

/* Callbacks */

/* Fade animation callback - simply resets thumbnail
 * 'fade_active' status */
static void gfx_thumbnail_fade_cb(void *userdata)
{
   gfx_thumbnail_t *thumbnail = (gfx_thumbnail_t*)userdata;
   if (thumbnail)
      thumbnail->flags |= GFX_THUMB_FLAG_FADE_ACTIVE;
}

/* Initialises thumbnail 'fade in' animation */
static void gfx_thumbnail_init_fade(
      gfx_thumbnail_state_t *p_gfx_thumb,
      gfx_thumbnail_t *thumbnail)
{
   /* Sanity check */
   if (!thumbnail)
      return;

   /* A 'fade in' animation is triggered if:
    * - Thumbnail is available
    * - Thumbnail is missing and 'fade_missing' is enabled */
   if (   (GFX_THUMB_STATUS_LOAD(&thumbnail->status) == GFX_THUMBNAIL_STATUS_AVAILABLE)
       || (p_gfx_thumb->fade_missing
       && (GFX_THUMB_STATUS_LOAD(&thumbnail->status) == GFX_THUMBNAIL_STATUS_MISSING)))
   {
      if (p_gfx_thumb->fade_duration > 0.0f)
      {
         gfx_animation_ctx_entry_t animation_entry;

         thumbnail->alpha                 = 0.0f;
         thumbnail->flags                |= GFX_THUMB_FLAG_FADE_ACTIVE;

         animation_entry.easing_enum      = EASING_OUT_QUAD;
         animation_entry.tag              = (uintptr_t)&thumbnail->alpha;
         animation_entry.duration         = p_gfx_thumb->fade_duration;
         animation_entry.target_value     = 1.0f;
         animation_entry.subject          = &thumbnail->alpha;
         animation_entry.cb               = gfx_thumbnail_fade_cb;
         animation_entry.userdata         = thumbnail;

         gfx_animation_push(&animation_entry);
      }
      else
         thumbnail->alpha = 1.0f;
   }
}

/* Used to process thumbnail data following completion
 * of image load task */
static void gfx_thumbnail_handle_upload(
      retro_task_t *task, void *task_data, void *user_data, const char *err)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
   struct texture_image *img          = (struct texture_image*)task_data;
   gfx_thumbnail_tag_t *thumbnail_tag = (gfx_thumbnail_tag_t*)user_data;
   bool fade_enabled                  = false;

   /* Sanity check */
   if (!thumbnail_tag)
      goto end;

   /* Ensure that we are operating on the correct
    * thumbnail... */
   if (thumbnail_tag->list_id != p_gfx_thumb->list_id)
      goto end;

   /* Only process image if we are waiting for it */
   if (GFX_THUMB_STATUS_LOAD(&thumbnail_tag->thumbnail->status) != GFX_THUMBNAIL_STATUS_PENDING)
      goto end;

   /* Sanity check: if thumbnail already has a texture,
    * we're in some kind of weird error state - in this
    * case, the best course of action is to just reset
    * the thumbnail... */
   if (thumbnail_tag->thumbnail->texture)
      gfx_thumbnail_reset(thumbnail_tag->thumbnail);

   /* Set thumbnail 'missing' status by default
    * (saves a number of checks later)
    * > Release-store ensures prior texture reset is
    *   visible before status change */
   GFX_THUMB_STATUS_STORE(&thumbnail_tag->thumbnail->status,
         GFX_THUMBNAIL_STATUS_MISSING);

   /* If we reach this stage, thumbnail 'fade in'
    * animations should be applied (based on current
    * thumbnail status and global configuration) */
   fade_enabled = true;

   /* Check we have a valid image */
   if (!img || (img->width < 1) || (img->height < 1))
      goto end;

   /* Upload texture to GPU */
   if (!video_driver_texture_load(
            img, TEXTURE_FILTER_LINEAR,
            &thumbnail_tag->thumbnail->texture))
      goto end;

   /* Cache dimensions */
   thumbnail_tag->thumbnail->width  = img->width;
   thumbnail_tag->thumbnail->height = img->height;

   /* Update thumbnail status
    * > Release-store ensures texture/width/height writes
    *   are visible to the video thread before it sees
    *   AVAILABLE via acquire-load in gfx_thumbnail_draw() */
   GFX_THUMB_STATUS_STORE(&thumbnail_tag->thumbnail->status,
         GFX_THUMBNAIL_STATUS_AVAILABLE);

end:
   /* Clean up */
   if (img)
   {
      image_texture_free(img);
      free(img);
   }

   if (thumbnail_tag)
   {
      /* Trigger 'fade in' animation, if required */
      if (fade_enabled)
         gfx_thumbnail_init_fade(p_gfx_thumb,
               thumbnail_tag->thumbnail);

      free(thumbnail_tag);
   }
}

/* Core interface */

/* When called, prevents the handling of any pending
 * thumbnail load requests
 * >> **MUST** be called before deleting any gfx_thumbnail_t
 *    objects passed to gfx_thumbnail_request() or
 *    gfx_thumbnail_process_stream(), otherwise
 *    heap-use-after-free errors *will* occur */
void gfx_thumbnail_cancel_pending_requests(void)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   p_gfx_thumb->list_id++;
}

/* Fetches the current thumbnail file path of the
 * specified thumbnail 'type'.
 * Returns true if path is valid. */
static bool gfx_thumbnail_get_path(
      gfx_thumbnail_path_data_t *path_data,
      enum gfx_thumbnail_id thumbnail_id,
      const char **path)
{
   if (path_data && path)
   {
      switch (thumbnail_id)
      {
         case GFX_THUMBNAIL_RIGHT:
            if (*path_data->right_path)
            {
               *path          = path_data->right_path;
               return true;
            }
            break;
         case GFX_THUMBNAIL_LEFT:
            if (*path_data->left_path)
            {
               *path          = path_data->left_path;
               return true;
            }
            break;
         case GFX_THUMBNAIL_ICON:
            if (*path_data->icon_path)
            {
               *path          = path_data->icon_path;
               return true;
            }
            break;
         default:
            break;
      }
   }

   return false;
}


/* Requests loading of the specified thumbnail
 * - If operation fails, 'thumbnail->status' will be set to
 *   GFX_THUMBNAIL_STATUS_MISSING
 * - If operation is successful, 'thumbnail->status' will be
 *   set to GFX_THUMBNAIL_STATUS_PENDING
 * 'thumbnail' will be populated with texture info/metadata
 * once the image load is complete
 * NOTE 1: Must be called *after* gfx_thumbnail_set_system()
 *         and gfx_thumbnail_set_content*()
 * NOTE 2: 'playlist' and 'idx' are only required here for
 *         on-demand thumbnail download support
 *         (an annoyance...) */
void gfx_thumbnail_request(
      gfx_thumbnail_path_data_t *path_data,
      enum gfx_thumbnail_id thumbnail_id,
      playlist_t *playlist,
      size_t idx,
      gfx_thumbnail_t *thumbnail,
      unsigned gfx_thumbnail_upscale_threshold,
      bool network_on_demand_thumbnails)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   if (!path_data || !thumbnail)
      return;

   /* Reset thumbnail, then set 'missing' status by default
    * (saves a number of checks later) */
   gfx_thumbnail_reset(thumbnail);
   GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);

   /* Update/extract thumbnail path */
   if (gfx_thumbnail_is_enabled(path_data, thumbnail_id))
   {
      if (gfx_thumbnail_update_path(path_data, thumbnail_id))
      {
         const char *thumbnail_path = NULL;
         if (gfx_thumbnail_get_path(path_data, thumbnail_id, &thumbnail_path))
         {
            /* Load thumbnail, if required */
            if (path_is_valid(thumbnail_path))
            {
               gfx_thumbnail_tag_t *thumbnail_tag =
                  (gfx_thumbnail_tag_t*)malloc(sizeof(gfx_thumbnail_tag_t));

               if (!thumbnail_tag)
                  goto end;

               /* Configure user data */
               thumbnail_tag->thumbnail = thumbnail;
               thumbnail_tag->list_id   = p_gfx_thumb->list_id;

               /* Would like to cancel any existing image load tasks
                * here, but can't see how to do it... */
               if (task_push_image_load(
                        thumbnail_path, (video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA),
                        gfx_thumbnail_upscale_threshold,
                        gfx_thumbnail_handle_upload, thumbnail_tag))
                  GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_PENDING);
            }
#ifdef HAVE_NETWORKING
            /* Handle on demand thumbnail downloads */
            else if (network_on_demand_thumbnails)
            {
               enum playlist_thumbnail_name_flags curr_flag;
               static char last_img_name[PATH_MAX_LENGTH] = {0};
               bool playlist_use_filename                 = config_get_ptr()->bools.playlist_use_filename;
               if (!playlist)
                  goto end;
               /* Only trigger a thumbnail download if image
                * name has changed since the last call of
                * gfx_thumbnail_request()
                * > Allows gfx_thumbnail_request() to be used
                *   for successive right/left thumbnail requests
                *   with minimal duplication of effort
                *   (i.e. task_push_pl_entry_thumbnail_download()
                *   will automatically cancel if a download for the
                *   existing playlist entry is pending, but the
                *   checks required for this involve significant
                *   overheads. We can avoid this entirely with
                *   a simple string comparison) */
               if (*path_data->content_img)
                  if (string_is_equal(path_data->content_img, last_img_name))
                     goto end;

               strlcpy(last_img_name, path_data->content_img, sizeof(last_img_name));

               /* Get system name */
               if (!*path_data->system)
                  goto end;

               /* Since task_push_pl_entry_download will shift the flag, do not attempt if it is already
                * at second to last option. */
               curr_flag = playlist_get_curr_thumbnail_name_flag(playlist,idx);
               if (   curr_flag & PLAYLIST_THUMBNAIL_FLAG_NONE
                   || curr_flag & PLAYLIST_THUMBNAIL_FLAG_SHORT_NAME)
                  goto end;
               /* Do not try to fetch full names here, if it is not explicitly wanted */
               if (   !playlist_use_filename
                   && !playlist_thumbnail_match_with_filename(playlist)
                   && curr_flag == PLAYLIST_THUMBNAIL_FLAG_INVALID)
                    playlist_update_thumbnail_name_flag(playlist, idx, PLAYLIST_THUMBNAIL_FLAG_FULL_NAME);

               /* Trigger thumbnail download *
                * Note: download will grab all 3 possible thumbnails, no matter
                * what left/right thumbnails are set at the moment */
               task_push_pl_entry_thumbnail_download(path_data->system, playlist,
                     (unsigned)idx, false, true);
            }
#endif
         }
      }
   }

end:
   /* Trigger 'fade in' animation, if required */
   if (GFX_THUMB_STATUS_LOAD(&thumbnail->status) != GFX_THUMBNAIL_STATUS_PENDING)
      gfx_thumbnail_init_fade(p_gfx_thumb,
            thumbnail);
}

/* Requests loading of a specific thumbnail image file
 * (may be used, for example, to load savestate images)
 * - If operation fails, 'thumbnail->status' will be set to
 *   MUI_THUMBNAIL_STATUS_MISSING
 * - If operation is successful, 'thumbnail->status' will be
 *   set to MUI_THUMBNAIL_STATUS_PENDING
 * 'thumbnail' will be populated with texture info/metadata
 * once the image load is complete */
void gfx_thumbnail_request_file(
      const char *file_path, gfx_thumbnail_t *thumbnail,
      unsigned gfx_thumbnail_upscale_threshold)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
   gfx_thumbnail_tag_t *thumbnail_tag = NULL;

   if (!thumbnail)
      return;

   /* Reset thumbnail, then set 'missing' status by default
    * (saves a number of checks later) */
   gfx_thumbnail_reset(thumbnail);
   GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);

   /* Check if file path is valid */
   if (   (!file_path || !*file_path)
       || !path_is_valid(file_path))
      return;

   /* Load thumbnail */
   if (!(thumbnail_tag = (gfx_thumbnail_tag_t*)malloc(sizeof(gfx_thumbnail_tag_t))))
      return;

   /* Configure user data */
   thumbnail_tag->thumbnail = thumbnail;
   thumbnail_tag->list_id   = p_gfx_thumb->list_id;

   /* Would like to cancel any existing image load tasks
    * here, but can't see how to do it... */
   if (task_push_image_load(
         file_path, (video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA),
         gfx_thumbnail_upscale_threshold,
         gfx_thumbnail_handle_upload, thumbnail_tag))
      GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_PENDING);
}

/* Resets (and free()s the current texture of) the
 * specified thumbnail */
void gfx_thumbnail_reset(gfx_thumbnail_t *thumbnail)
{
   if (!thumbnail)
      return;

   /* Unload texture */
   if (thumbnail->texture)
      video_driver_texture_unload(&thumbnail->texture);

   /* Ensure any 'fade in' animation is killed */
   if (thumbnail->flags & GFX_THUMB_FLAG_FADE_ACTIVE)
   {
      uintptr_t tag = (uintptr_t)&thumbnail->alpha;
      gfx_animation_kill_by_tag(&tag);
   }

   /* Reset all parameters */
   GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_UNKNOWN);
   thumbnail->texture     = 0;
   thumbnail->width       = 0;
   thumbnail->height      = 0;
   thumbnail->alpha       = 0.0f;
   thumbnail->delay_timer = 0.0f;
   thumbnail->flags      &= ~(GFX_THUMB_FLAG_FADE_ACTIVE
                            | GFX_THUMB_FLAG_CORE_ASPECT);
}

/* Stream processing */

/* Requests loading of the specified thumbnail via
 * the stream interface
 * - Must be called on each frame for the duration
 *   that specified thumbnail is on-screen
 * - Actual load request is deferred by currently
 *   set stream delay
 * - Function becomes a no-op once load request is
 *   made
 * - Thumbnails loaded via this function must be
 *   deleted manually via gfx_thumbnail_reset()
 *   when they move off-screen
 * NOTE 1: Must be called *after* gfx_thumbnail_set_system()
 *         and gfx_thumbnail_set_content*()
 * NOTE 2: 'playlist' and 'idx' are only required here for
 *         on-demand thumbnail download support
 *         (an annoyance...)
 * NOTE 3: This function is intended for use in situations
 *         where each menu entry has a *single* thumbnail.
 *         If each entry has two thumbnails, use
 *         gfx_thumbnail_request_streams() for improved
 *         performance */
void gfx_thumbnail_request_stream(
      gfx_thumbnail_path_data_t *path_data,
      gfx_animation_t *p_anim,
      enum gfx_thumbnail_id thumbnail_id,
      playlist_t *playlist, size_t idx,
      gfx_thumbnail_t *thumbnail,
      unsigned gfx_thumbnail_upscale_threshold,
      bool network_on_demand_thumbnails)
{
   gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

   /* Only process request if current status
    * is GFX_THUMBNAIL_STATUS_UNKNOWN */
   if (   !thumbnail
       || (GFX_THUMB_STATUS_LOAD(&thumbnail->status) != GFX_THUMBNAIL_STATUS_UNKNOWN))
      return;

   /* Check if stream delay timer has elapsed */
   thumbnail->delay_timer += p_anim->delta_time;

   if (thumbnail->delay_timer > p_gfx_thumb->stream_delay)
   {
      /* Sanity check */
      if (!path_data)
      {
         /* No path information
          * > Reset thumbnail and set missing status
          *   to prevent repeated load attempts */
         gfx_thumbnail_reset(thumbnail);
         GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
         thumbnail->alpha  = 1.0f;
         return;
      }

      /* Request image load */
      gfx_thumbnail_request(
            path_data, thumbnail_id, playlist, idx, thumbnail,
            gfx_thumbnail_upscale_threshold,
            network_on_demand_thumbnails);
   }
}

/* Requests loading of the specified thumbnails via
 * the stream interface
 * - Must be called on each frame for the duration
 *   that specified thumbnails are on-screen
 * - Actual load request is deferred by currently
 *   set stream delay
 * - Function becomes a no-op once load request is
 *   made
 * - Thumbnails loaded via this function must be
 *   deleted manually via gfx_thumbnail_reset()
 *   when they move off-screen
 * NOTE 1: Must be called *after* gfx_thumbnail_set_system()
 *         and gfx_thumbnail_set_content*()
 * NOTE 2: 'playlist' and 'idx' are only required here for
 *         on-demand thumbnail download support
 *         (an annoyance...)
 * NOTE 3: This function is intended for use in situations
 *         where each menu entry has *two* thumbnails.
 *         If each entry only has a single thumbnail, use
 *         gfx_thumbnail_request_stream() for improved
 *         performance */
void gfx_thumbnail_request_streams(
      gfx_thumbnail_path_data_t *path_data,
      gfx_animation_t *p_anim,
      playlist_t *playlist, size_t idx,
      gfx_thumbnail_t *right_thumbnail,
      gfx_thumbnail_t *left_thumbnail,
      unsigned gfx_thumbnail_upscale_threshold,
      bool network_on_demand_thumbnails)
{
   bool process_r = false;
   bool process_l = false;

   if (!right_thumbnail || !left_thumbnail)
      return;

   /* Only process request if current status
    * is GFX_THUMBNAIL_STATUS_UNKNOWN */
   process_r = (GFX_THUMB_STATUS_LOAD(&right_thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN);
   process_l = (GFX_THUMB_STATUS_LOAD(&left_thumbnail->status)  == GFX_THUMBNAIL_STATUS_UNKNOWN);

   if (process_r || process_l)
   {
      /* Check if stream delay timer has elapsed */
      gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
      float delta_time                   = p_anim->delta_time;
      bool request_r                     = false;
      bool request_l                     = false;

      if (process_r)
      {
         right_thumbnail->delay_timer += delta_time;
         request_r                     =
               (right_thumbnail->delay_timer > p_gfx_thumb->stream_delay);
      }

      if (process_l)
      {
         left_thumbnail->delay_timer  += delta_time;
         request_l                     =
               (left_thumbnail->delay_timer > p_gfx_thumb->stream_delay);
      }

      /* Check if one or more thumbnails should be requested */
      if (request_r || request_l)
      {
         /* Sanity check */
         if (!path_data)
         {
            /* No path information
             * > Reset thumbnail and set missing status
             *   to prevent repeated load attempts */
            if (request_r)
            {
               gfx_thumbnail_reset(right_thumbnail);
               GFX_THUMB_STATUS_STORE(&right_thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
               right_thumbnail->alpha  = 1.0f;
            }

            if (request_l)
            {
               gfx_thumbnail_reset(left_thumbnail);
               GFX_THUMB_STATUS_STORE(&left_thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
               left_thumbnail->alpha   = 1.0f;
            }

            return;
         }

         /* Request image load */
         if (request_r)
            gfx_thumbnail_request(
                  path_data, GFX_THUMBNAIL_RIGHT, playlist, idx, right_thumbnail,
                  gfx_thumbnail_upscale_threshold,
                  network_on_demand_thumbnails);

         if (request_l)
            gfx_thumbnail_request(
                  path_data, GFX_THUMBNAIL_LEFT, playlist, idx, left_thumbnail,
                  gfx_thumbnail_upscale_threshold,
                  network_on_demand_thumbnails);
      }
   }
}

/* Handles streaming of the specified thumbnail as it moves
 * on/off screen
 * - Must be called each frame for every on-screen entry
 * - Must be called once for each entry as it moves off-screen
 *   (or can be called each frame - overheads are small)
 * NOTE 1: Must be called *after* gfx_thumbnail_set_system()
 * NOTE 2: This function calls gfx_thumbnail_set_content*()
 * NOTE 3: This function is intended for use in situations
 *         where each menu entry has a *single* thumbnail.
 *         If each entry has two thumbnails, use
 *         gfx_thumbnail_process_streams() for improved
 *         performance */
void gfx_thumbnail_process_stream(
      gfx_thumbnail_path_data_t *path_data,
      gfx_animation_t *p_anim,
      enum gfx_thumbnail_id thumbnail_id,
      playlist_t *playlist, size_t idx,
      gfx_thumbnail_t *thumbnail,
      bool on_screen,
      unsigned gfx_thumbnail_upscale_threshold,
      bool network_on_demand_thumbnails)
{
   if (!thumbnail)
      return;

   if (on_screen)
   {
      /* Entry is on-screen
       * > Only process if current status is
       *   GFX_THUMBNAIL_STATUS_UNKNOWN */
      if (GFX_THUMB_STATUS_LOAD(&thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN)
      {
         gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;

         /* Check if stream delay timer has elapsed */
         thumbnail->delay_timer += p_anim->delta_time;

         if (thumbnail->delay_timer > p_gfx_thumb->stream_delay)
         {
            /* Update thumbnail content */
            if (   !path_data
                || !playlist
                || !gfx_thumbnail_set_content_playlist(path_data, playlist, idx))
            {
               /* Content is invalid
                * > Reset thumbnail and set missing status */
               gfx_thumbnail_reset(thumbnail);
               GFX_THUMB_STATUS_STORE(&thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
               thumbnail->alpha  = 1.0f;
               return;
            }

            /* Request image load */
            gfx_thumbnail_request(
                  path_data, thumbnail_id, playlist, idx, thumbnail,
                  gfx_thumbnail_upscale_threshold,
                  network_on_demand_thumbnails);
         }
      }
   }
   else
   {
      /* Entry is off-screen
       * > If status is GFX_THUMBNAIL_STATUS_UNKNOWN,
       *   thumbnail is already in a blank state - but we
       *   must ensure that delay timer is set to zero */
      if (GFX_THUMB_STATUS_LOAD(&thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN)
         thumbnail->delay_timer = 0.0f;
      /* In all other cases, reset thumbnail */
      else
         gfx_thumbnail_reset(thumbnail);
   }
}

/* Handles streaming of the specified thumbnails as they move
 * on/off screen
 * - Must be called each frame for every on-screen entry
 * - Must be called once for each entry as it moves off-screen
 *   (or can be called each frame - overheads are small)
 * NOTE 1: Must be called *after* gfx_thumbnail_set_system()
 * NOTE 2: This function calls gfx_thumbnail_set_content*()
 * NOTE 3: This function is intended for use in situations
 *         where each menu entry has *two* thumbnails.
 *         If each entry only has a single thumbnail, use
 *         gfx_thumbnail_process_stream() for improved
 *         performance */
void gfx_thumbnail_process_streams(
      gfx_thumbnail_path_data_t *path_data,
      gfx_animation_t *p_anim,
      playlist_t *playlist, size_t idx,
      gfx_thumbnail_t *right_thumbnail,
      gfx_thumbnail_t *left_thumbnail,
      bool on_screen,
      unsigned gfx_thumbnail_upscale_threshold,
      bool network_on_demand_thumbnails)
{
   if (!right_thumbnail || !left_thumbnail)
      return;

   if (on_screen)
   {
      /* Entry is on-screen
       * > Only process if current status is
       *   GFX_THUMBNAIL_STATUS_UNKNOWN */
      bool process_r = (GFX_THUMB_STATUS_LOAD(&right_thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN);
      bool process_l = (GFX_THUMB_STATUS_LOAD(&left_thumbnail->status)  == GFX_THUMBNAIL_STATUS_UNKNOWN);

      if (process_r || process_l)
      {
         /* Check if stream delay timer has elapsed */
         gfx_thumbnail_state_t *p_gfx_thumb = &gfx_thumb_st;
         float delta_time                   = p_anim->delta_time;
         bool request_r                     = false;
         bool request_l                     = false;

         if (process_r)
         {
            right_thumbnail->delay_timer += delta_time;
            request_r                     =
                  (right_thumbnail->delay_timer > p_gfx_thumb->stream_delay);
         }

         if (process_l)
         {
            left_thumbnail->delay_timer  += delta_time;
            request_l                     =
                  (left_thumbnail->delay_timer > p_gfx_thumb->stream_delay);
         }

         /* Check if one or more thumbnails should be requested */
         if (request_r || request_l)
         {
            /* Update thumbnail content */
            if (   !path_data
                || !playlist
                || !gfx_thumbnail_set_content_playlist(path_data, playlist, idx))
            {
               /* Content is invalid
                * > Reset thumbnail and set missing status */
               if (request_r)
               {
                  gfx_thumbnail_reset(right_thumbnail);
                  GFX_THUMB_STATUS_STORE(&right_thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
                  right_thumbnail->alpha  = 1.0f;
               }

               if (request_l)
               {
                  gfx_thumbnail_reset(left_thumbnail);
                  GFX_THUMB_STATUS_STORE(&left_thumbnail->status, GFX_THUMBNAIL_STATUS_MISSING);
                  left_thumbnail->alpha   = 1.0f;
               }

               return;
            }

            /* Request image load */
            if (request_r)
               gfx_thumbnail_request(
                     path_data, GFX_THUMBNAIL_RIGHT, playlist, idx, right_thumbnail,
                     gfx_thumbnail_upscale_threshold,
                     network_on_demand_thumbnails);

            if (request_l)
               gfx_thumbnail_request(
                     path_data, GFX_THUMBNAIL_LEFT, playlist, idx, left_thumbnail,
                     gfx_thumbnail_upscale_threshold,
                     network_on_demand_thumbnails);
         }
      }
   }
   else
   {
      /* Entry is off-screen
       * > If status is GFX_THUMBNAIL_STATUS_UNKNOWN,
       *   thumbnail is already in a blank state - but we
       *   must ensure that delay timer is set to zero
       * > In all other cases, reset thumbnail */
      if (GFX_THUMB_STATUS_LOAD(&right_thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN)
         right_thumbnail->delay_timer = 0.0f;
      else
         gfx_thumbnail_reset(right_thumbnail);

      if (GFX_THUMB_STATUS_LOAD(&left_thumbnail->status) == GFX_THUMBNAIL_STATUS_UNKNOWN)
         left_thumbnail->delay_timer = 0.0f;
      else
         gfx_thumbnail_reset(left_thumbnail);
   }
}

/* Thumbnail rendering */

/* Determines the actual screen dimensions of a
 * thumbnail when centred with aspect correct
 * scaling within a rectangle of (width x height) */
void gfx_thumbnail_get_draw_dimensions(
      gfx_thumbnail_t *thumbnail,
      unsigned width, unsigned height, float scale_factor,
      float *draw_width, float *draw_height)
{
   float core_aspect;
   float display_aspect;
   float thumbnail_aspect;
   video_driver_state_t *video_st = video_state_get_ptr();

   /* Sanity check */
   if (   !thumbnail
       || (width             < 1)
       || (height            < 1)
       || (thumbnail->width  < 1)
       || (thumbnail->height < 1))
   {
      *draw_width  = 0.0f;
      *draw_height = 0.0f;
      return;
   }

   /* Account for display/thumbnail/core aspect ratio
    * differences */
   display_aspect   = (float)width            / (float)height;
   thumbnail_aspect = (float)thumbnail->width / (float)thumbnail->height;
   core_aspect      = ((thumbnail->flags & GFX_THUMB_FLAG_CORE_ASPECT)
         && video_st && video_st->av_info.geometry.aspect_ratio > 0)
               ? video_st->av_info.geometry.aspect_ratio
               : thumbnail_aspect;

   if (thumbnail_aspect > display_aspect)
   {
      *draw_width  = (float)width;
      *draw_height = (float)thumbnail->height * (*draw_width / (float)thumbnail->width);

      if (thumbnail->flags & GFX_THUMB_FLAG_CORE_ASPECT)
      {
         *draw_height = *draw_height * (thumbnail_aspect / core_aspect);

         if (*draw_height > height)
         {
            *draw_height = (float)height;
            *draw_width  = (float)thumbnail->width * (*draw_height / (float)thumbnail->height);
            *draw_width  = *draw_width / (thumbnail_aspect / core_aspect);
         }
      }
   }
   else
   {
      *draw_height = (float)height;
      *draw_width  = (float)thumbnail->width * (*draw_height / (float)thumbnail->height);

      if (thumbnail->flags & GFX_THUMB_FLAG_CORE_ASPECT)
         *draw_width  = *draw_width / (thumbnail_aspect / core_aspect);
   }

   /* Final overwidth check */
   if (*draw_width > width)
   {
      *draw_width  = (float)width;
      *draw_height = (float)thumbnail->height * (*draw_width / (float)thumbnail->width);

      if (thumbnail->flags & GFX_THUMB_FLAG_CORE_ASPECT)
         *draw_height = *draw_height * (thumbnail_aspect / core_aspect);
   }

   /* Account for scale factor
    * > Side note: We cannot use the gfx_display_ctx_draw_t
    *   'scale_factor' parameter for scaling thumbnails,
    *   since this clips off any part of the expanded image
    *   that extends beyond the bounding box. But even if
    *   it didn't, we can't get real screen dimensions
    *   without scaling manually... */
   *draw_width  *= scale_factor;
   *draw_height *= scale_factor;
}

/* Draws specified thumbnail with specified alignment
 * (and aspect correct scaling) within a rectangle of
 * (width x height).
 * 'shadow' defines an optional shadow effect (may be
 * set to NULL if a shadow effect is not required).
 * NOTE: Setting scale_factor > 1.0f will increase the
 *       size of the thumbnail beyond the limits of the
 *       (width x height) rectangle (alignment + aspect
 *       correct scaling is preserved). Use with caution */

void gfx_thumbnail_draw(
      void *userdata,
      unsigned video_width,
      unsigned video_height,
      gfx_thumbnail_t *thumbnail,
      float x, float y, unsigned width, unsigned height,
      enum gfx_thumbnail_alignment alignment,
      float alpha, float scale_factor,
      gfx_thumbnail_shadow_t *shadow)
{
   gfx_display_t            *p_disp  = disp_get_ptr();
   gfx_display_ctx_driver_t *dispctx = p_disp->dispctx;
   /* Sanity check */
   if (
            !thumbnail
         || !dispctx
         || (width         < 1)
         || (height        < 1)
         || (alpha        <= 0.0f)
         || (scale_factor <= 0.0f)
      )
      return;

   /* Only draw thumbnail if it is available...
    * > Acquire-load pairs with release-store in
    *   gfx_thumbnail_handle_upload() to ensure
    *   texture/width/height are visible when we
    *   read AVAILABLE */
   if (GFX_THUMB_STATUS_LOAD(&thumbnail->status) == GFX_THUMBNAIL_STATUS_AVAILABLE)
   {
      gfx_display_ctx_draw_t draw;
      struct video_coords coords;
      math_matrix_4x4 mymat;
      float draw_width;
      float draw_height;
      float draw_x;
      float draw_y;

      /* Snapshot fields — read once, use local copies for the
       * remainder of this draw call.  The main thread may update
       * the live struct concurrently (upload callback, reset, or
       * animation tick on alpha), but these locals are stable. */
      uintptr_t thumb_texture = thumbnail->texture;
      unsigned  thumb_width   = thumbnail->width;
      unsigned  thumb_height  = thumbnail->height;
      float     thumb_alpha   = thumbnail->alpha;
      uint8_t   thumb_flags   = thumbnail->flags;

      float thumbnail_alpha     = thumb_alpha * alpha;
      float thumbnail_color[16] = {
         1.0f, 1.0f, 1.0f, 1.0f,
         1.0f, 1.0f, 1.0f, 1.0f,
         1.0f, 1.0f, 1.0f, 1.0f,
         1.0f, 1.0f, 1.0f, 1.0f
      };

      /* Set thumbnail opacity */
      if (thumbnail_alpha <= 0.0f)
         return;
      if (thumbnail_alpha < 1.0f)
         gfx_display_set_alpha(thumbnail_color, thumbnail_alpha);

      /* Get thumbnail dimensions using snapshot values
       * > Build a temporary on the stack so
       *   gfx_thumbnail_get_draw_dimensions reads
       *   consistent data without touching the live struct */
      {
         gfx_thumbnail_t thumb_snapshot;
         thumb_snapshot.texture = thumb_texture;
         thumb_snapshot.width   = thumb_width;
         thumb_snapshot.height  = thumb_height;
         thumb_snapshot.alpha   = thumb_alpha;
         thumb_snapshot.flags   = thumb_flags;
         /* Local snapshot: initialise the atomic-typed status
          * field via _init since this is its first write --
          * direct assignment to the C11/C++11 atomic forms is
          * illegal.  This local is never observed by another
          * thread, so no ordering is required. */
         retro_atomic_int_init(&thumb_snapshot.status, GFX_THUMBNAIL_STATUS_AVAILABLE);
         thumb_snapshot.delay_timer = 0.0f;
         gfx_thumbnail_get_draw_dimensions(
               &thumb_snapshot, width, height, scale_factor,
               &draw_width, &draw_height);
      }

      if (dispctx->blend_begin)
         dispctx->blend_begin(userdata);

      if (!p_disp->dispctx->handles_transform)
      {
         /* Perform 'rotation' step
          * > Note that rotation does not actually work...
          * > It rotates the image all right, but distorts it
          *   to fit the aspect of the bounding box while clipping
          *   off any 'corners' that extend beyond the bounding box
          * > Since the result is visual garbage, we disable
          *   rotation entirely
          * > But we still have to call gfx_display_rotate_z(),
          *   or nothing will be drawn...
          */
         float cosine             = 1.0f; /* cos(rad)  = cos(0)  = 1.0f */
         float sine               = 0.0f; /* sine(rad) = sine(0) = 0.0f */
         gfx_display_rotate_z(p_disp, &mymat, cosine, sine, userdata);
      }

      /* Configure draw object
       * > Note: Colour, width/height and position must
       *   be set *after* drawing any shadow effects */
      coords.vertices      = 4;
      coords.vertex        = NULL;
      coords.tex_coord     = NULL;
      coords.lut_tex_coord = NULL;

      draw.scale_factor    = 1.0f;
      draw.rotation        = 0.0f;
      draw.coords          = &coords;
      draw.matrix_data     = &mymat;
      draw.texture         = thumb_texture;
      draw.pipeline_id     = 0;

      /* Set thumbnail alignment within bounding box */
      switch (alignment)
      {
         case GFX_THUMBNAIL_ALIGN_TOP:
            /* Centred horizontally */
            draw_x = x + ((float)width - draw_width) / 2.0f;
            /* Drawn at top of bounding box */
            draw_y = (float)video_height - y - draw_height;
            break;
         case GFX_THUMBNAIL_ALIGN_BOTTOM:
            /* Centred horizontally */
            draw_x = x + ((float)width - draw_width) / 2.0f;
            /* Drawn at bottom of bounding box */
            draw_y = (float)video_height - y - (float)height;
            break;
         case GFX_THUMBNAIL_ALIGN_LEFT:
            /* Drawn at left side of bounding box */
            draw_x = x;
            /* Centred vertically */
            draw_y = (float)video_height - y - draw_height - ((float)height - draw_height) / 2.0f;
            break;
         case GFX_THUMBNAIL_ALIGN_RIGHT:
            /* Drawn at right side of bounding box */
            draw_x = x + (float)width - draw_width;
            /* Centred vertically */
            draw_y = (float)video_height - y - draw_height - ((float)height - draw_height) / 2.0f;
            break;
         case GFX_THUMBNAIL_ALIGN_CENTRE:
         default:
            /* Centred both horizontally and vertically */
            draw_x = x + ((float)width - draw_width) / 2.0f;
            draw_y = (float)video_height - y - draw_height - ((float)height - draw_height) / 2.0f;
            break;
      }

      /* Draw shadow effect, if required */
      if (shadow)
      {
         /* Sanity check */
         if (     (shadow->type != GFX_THUMBNAIL_SHADOW_NONE)
               && (shadow->alpha > 0.0f))
         {
            float shadow_width;
            float shadow_height;
            float shadow_x;
            float shadow_y;
            float shadow_color[16] = {
               0.0f, 0.0f, 0.0f, 1.0f,
               0.0f, 0.0f, 0.0f, 1.0f,
               0.0f, 0.0f, 0.0f, 1.0f,
               0.0f, 0.0f, 0.0f, 1.0f
            };
            float shadow_alpha     = thumbnail_alpha;

            /* Set shadow opacity */
            if (shadow->alpha < 1.0f)
               shadow_alpha *= shadow->alpha;

            gfx_display_set_alpha(shadow_color, shadow_alpha);

            /* Configure shadow based on effect type
             * > Not using a switch() here, since we've
             *   already eliminated GFX_THUMBNAIL_SHADOW_NONE */
            if (shadow->type == GFX_THUMBNAIL_SHADOW_OUTLINE)
            {
               shadow_width  = draw_width  + (float)(shadow->outline.width * 2);
               shadow_height = draw_height + (float)(shadow->outline.width * 2);
               shadow_x      = draw_x - (float)shadow->outline.width;
               shadow_y      = draw_y - (float)shadow->outline.width;
            }
            /* Default: GFX_THUMBNAIL_SHADOW_DROP */
            else
            {
               shadow_width  = draw_width;
               shadow_height = draw_height;
               shadow_x      = draw_x + shadow->drop.x_offset;
               shadow_y      = draw_y - shadow->drop.y_offset;
            }

            /* Apply shadow draw object configuration */
            coords.color = (const float*)shadow_color;
            draw.width   = (unsigned)shadow_width;
            draw.height  = (unsigned)shadow_height;
            draw.x       = shadow_x;
            draw.y       = shadow_y;

            /* Draw shadow */
            if (draw.height > 0 && draw.width > 0)
               if (dispctx->draw)
                  dispctx->draw(&draw, userdata, video_width, video_height);
         }
      }

      /* Final thumbnail draw object configuration */
      coords.color = (const float*)thumbnail_color;
      draw.width   = (unsigned)draw_width;
      draw.height  = (unsigned)draw_height;
      draw.x       = draw_x;
      draw.y       = draw_y;

      /* Draw thumbnail */
      if (draw.height > 0 && draw.width > 0)
         if (dispctx->draw)
            dispctx->draw(&draw, userdata, video_width, video_height);

      if (dispctx->blend_end)
         dispctx->blend_end(userdata);
   }
}

/* Returns currently set thumbnail 'type' (Named_Snaps,
 * Named_Titles, Named_Boxarts, Named_Logos) for specified thumbnail
 * identifier (right, left) */
static const char *gfx_thumbnail_get_type(
      unsigned gfx_thumbnails,
      unsigned left_thumbnails,
      unsigned icon_thumbnails,
      gfx_thumbnail_path_data_t *path_data,
      enum gfx_thumbnail_id thumbnail_id)
{
   if (path_data)
   {
      unsigned type                 = 0;
      switch (thumbnail_id)
      {
         case GFX_THUMBNAIL_RIGHT:
            if (   path_data->playlist_right_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
               type = (unsigned)path_data->playlist_right_mode - 1;
            else
               type = gfx_thumbnails;
            break;
         case GFX_THUMBNAIL_LEFT:
            if (   path_data->playlist_left_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
               type = (unsigned)path_data->playlist_left_mode - 1;
            else
               type = left_thumbnails;
            break;
         case GFX_THUMBNAIL_ICON:
            if (   path_data->playlist_icon_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
               type = (unsigned)path_data->playlist_icon_mode - 1;
            else
               type = icon_thumbnails;
            break;
         default:
            goto end;
      }

      switch (type)
      {
         case 1:
            return "Named_Snaps";
         case 2:
            return "Named_Titles";
         case 3:
            return "Named_Boxarts";
         case 4:
            return "Named_Logos";
         case 0:
         default:
            break;
      }
   }

end:
   return msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF);
}

/* Fills content_img field of path_data using existing
 * content_label field (for internal use only) */
void gfx_thumbnail_fill_content_img(char *s,
   size_t len, const char *src, bool shorten)
{
   char *scrub_char_ptr = NULL;
   /* Copy source label string */
   size_t _len          = strlcpy(s, src, len);

   /* Shortening logic: up to first space + bracket */
   if (shorten)
   {
      int bracketpos    = -1;
      if ((bracketpos = string_find_index_substring_string(src, " (")) > 0)
         _len = bracketpos;
      /* Explicit zero if short name is same as standard name - saves some queries later. */
      else
      {
         s[0] = '\0';
         return;
      }
   }
   /* Scrub characters that are not cross-platform and/or violate the
    * No-Intro filename standard:
    * https://datomatic.no-intro.org/stuff/The%20Official%20No-Intro%20Convention%20(20071030).pdf
    * Replace these characters in the entry name with underscores */
   while ((scrub_char_ptr = strpbrk(s, "&*/:`\"<>?\\|")))
      *scrub_char_ptr = '_';
   /* Add PNG extension */
   strlcpy(s + _len, ".png", len - _len);
}

/* Resets thumbnail path data
 * (blanks all internal string containers) */
void gfx_thumbnail_path_reset(gfx_thumbnail_path_data_t *path_data)
{
   if (!path_data)
      return;

   path_data->system_len           = 0;
   path_data->system[0]            = '\0';
   path_data->content_path[0]      = '\0';
   path_data->content_label_len    = 0;
   path_data->content_label[0]     = '\0';
   path_data->content_core_name[0] = '\0';
   path_data->content_db_name[0]   = '\0';
   path_data->content_img[0]       = '\0';
   path_data->content_img_full[0]  = '\0';
   path_data->content_img_short[0] = '\0';
   path_data->right_path[0]        = '\0';
   path_data->left_path[0]         = '\0';
   path_data->icon_path[0]         = '\0';

   path_data->playlist_right_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_left_mode  = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_icon_mode  = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
}

/* Initialisation */

/* Creates new thumbnail path data container.
 * Returns handle to new gfx_thumbnail_path_data_t object.
 * on success, otherwise NULL.
 * Note: Returned object must be free()d */
gfx_thumbnail_path_data_t *gfx_thumbnail_path_init(void)
{
   /* Use calloc rather than malloc.  gfx_thumbnail_path_reset()
    * resets the string buffers and the playlist_*_mode fields
    * but does NOT touch playlist_index, leaving it as garbage
    * from malloc until the first set_content_*() call.  Read
    * sites in xmb (xmb_set_title) sample
    * thumbnail_path_data->playlist_index before any setter has
    * necessarily run, then pass it to playlist_get_index() --
    * which is bounds-checked, so a garbage index just returns
    * a stale pl_entry rather than crashing, but the code is
    * latently UB and a future read site without the same defence
    * would inherit a real bug.  Zero-init via calloc closes
    * the window without churning path_reset's API. */
   gfx_thumbnail_path_data_t *path_data = (gfx_thumbnail_path_data_t*)
      calloc(1, sizeof(*path_data));
   if (!path_data)
      return NULL;

   gfx_thumbnail_path_reset(path_data);

   return path_data;
}

/* Utility Functions */

/* Returns true if specified thumbnail is enabled
 * (i.e. if 'type' is not equal to MENU_ENUM_LABEL_VALUE_OFF) */
bool gfx_thumbnail_is_enabled(gfx_thumbnail_path_data_t *path_data,
      enum gfx_thumbnail_id thumbnail_id)
{
   if (path_data)
   {
      settings_t          *settings = config_get_ptr();
      unsigned gfx_thumbnails       = settings->uints.gfx_thumbnails;
      unsigned menu_left_thumbnails = settings->uints.menu_left_thumbnails;
      unsigned menu_icon_thumbnails = settings->uints.menu_icon_thumbnails;

      switch (thumbnail_id)
      {
         case GFX_THUMBNAIL_RIGHT:
            if (   path_data->playlist_right_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
               return path_data->playlist_right_mode != PLAYLIST_THUMBNAIL_MODE_OFF;
            return gfx_thumbnails != 0;
         case GFX_THUMBNAIL_LEFT:
            if (   path_data->playlist_left_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
               return path_data->playlist_left_mode != PLAYLIST_THUMBNAIL_MODE_OFF;
            return menu_left_thumbnails != 0;
         case GFX_THUMBNAIL_ICON:
            if (   path_data->playlist_icon_mode 
                != PLAYLIST_THUMBNAIL_MODE_DEFAULT)
                return path_data->playlist_icon_mode != PLAYLIST_THUMBNAIL_MODE_OFF;
            return menu_icon_thumbnails != 0;
         default:
            break;
      }
   }

   return false;
}

/* Setters */

/* Sets current 'system' (default database name).
 * Returns true if 'system' is valid.
 * If playlist is provided, extracts system-specific
 * thumbnail assignment metadata (required for accurate
 * usage of gfx_thumbnail_is_enabled())
 * > Used as a fallback when individual content lacks an
 *   associated database name */
bool gfx_thumbnail_set_system(gfx_thumbnail_path_data_t *path_data,
      const char *system, playlist_t *playlist)
{
   if (!path_data)
      return false;

   /* When system is updated, must regenerate right/left
    * thumbnail paths */
   path_data->right_path[0]       = '\0';
   path_data->left_path[0]        = '\0';

   /* 'Reset' path_data system string */
   path_data->system_len          = 0;
   path_data->system[0]           = '\0';

   /* Must also reset playlist thumbnail display modes */
   path_data->playlist_right_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_left_mode  = PLAYLIST_THUMBNAIL_MODE_DEFAULT;

   if (!system || !*system)
      return false;

   /* Hack: There is only one MAME thumbnail repo,
    * so filter any input starting with 'MAME...' */
   if (strncmp(system, "MAME", 4) == 0)
      path_data->system_len = strlcpy(path_data->system, "MAME", sizeof(path_data->system));
   else
      path_data->system_len = strlcpy(path_data->system, system, sizeof(path_data->system));

   /* Addendum: Now that we have per-playlist thumbnail display
    * modes, we must extract them here - otherwise
    * gfx_thumbnail_is_enabled() will go out of sync */
   if (playlist)
   {
      const char *playlist_path    = playlist_get_conf_path(playlist);

      /* Note: This is not considered an error
       * (just means that input playlist is ignored) */
      if (playlist_path && *playlist_path)
      {
         const char *playlist_file = path_basename_nocompression(playlist_path);
         /* Note: This is not considered an error
          * (just means that input playlist is ignored) */
         if (playlist_file && *playlist_file)
         {
            /* Check for history/favourites playlists */
            bool playlist_valid =
               (   memcmp(system, "history", 8) == 0
                && memcmp(playlist_file,
                   FILE_PATH_CONTENT_HISTORY,
                   sizeof(FILE_PATH_CONTENT_HISTORY)) == 0)
               || (     memcmp(system, "favorites", 10) == 0
                     && memcmp(playlist_file,
                        FILE_PATH_CONTENT_FAVORITES,
                        sizeof(FILE_PATH_CONTENT_FAVORITES)) == 0);

            /* This means we have to work a little harder
             * i.e. check whether the cached playlist file
             * matches the database name */
            if (!playlist_valid)
            {
               char playlist_name[NAME_MAX_LENGTH];
               fill_pathname(playlist_name, playlist_file, "", sizeof(playlist_name));
               playlist_valid = string_is_equal(playlist_name, system);
            }

            /* If we have a valid playlist, extract thumbnail modes */
            if (playlist_valid)
            {
               path_data->playlist_right_mode =
                  playlist_get_thumbnail_mode(playlist, PLAYLIST_THUMBNAIL_RIGHT);
               path_data->playlist_left_mode =
                  playlist_get_thumbnail_mode(playlist, PLAYLIST_THUMBNAIL_LEFT);
            }
         }
      }
   }

   return true;
}

/* Sets current thumbnail content according to the specified label.
 * Returns true if content is valid */
bool gfx_thumbnail_set_content(gfx_thumbnail_path_data_t *path_data, const char *label)
{
   if (!path_data)
      return false;

   /* When content is updated, must regenerate right/left
    * thumbnail paths */
   path_data->right_path[0]        = '\0';
   path_data->left_path[0]         = '\0';

   /* 'Reset' path_data content strings */
   path_data->content_path[0]      = '\0';
   path_data->content_label_len    = 0;
   path_data->content_label[0]     = '\0';
   path_data->content_core_name[0] = '\0';
   path_data->content_db_name[0]   = '\0';
   path_data->content_img[0]       = '\0';
   path_data->content_img_full[0]  = '\0';
   path_data->content_img_short[0] = '\0';

   /* Must also reset playlist thumbnail display modes */
   path_data->playlist_right_mode  = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_left_mode   = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_index       = 0;

   if (!label || !*label)
      return false;

   /* Cache content label */
   path_data->content_label_len = strlcpy(path_data->content_label,
         label, sizeof(path_data->content_label));

   /* Determine content image name */
   gfx_thumbnail_fill_content_img(path_data->content_img,
         sizeof(path_data->content_img),
         path_data->content_label, false);
   gfx_thumbnail_fill_content_img(path_data->content_img_short,
         sizeof(path_data->content_img_short),
         path_data->content_label, true);

   /* Have to set content path to *something*...
    * Just use label value (it doesn't matter) */
   strlcpy(path_data->content_path, label, sizeof(path_data->content_path));

   /* Redundant error check... */
   return *path_data->content_img;
}

/* Sets current thumbnail content to the specified image.
 * Returns true if content is valid */
bool gfx_thumbnail_set_content_image(
      gfx_thumbnail_path_data_t *path_data,
      const char *img_dir, const char *img_name)
{
   if (!path_data)
      return false;

   /* When content is updated, must regenerate right/left
    * thumbnail paths */
   path_data->right_path[0]        = '\0';
   path_data->left_path[0]         = '\0';

   /* 'Reset' path_data content strings */
   path_data->content_path[0]      = '\0';
   path_data->content_label_len    = 0;
   path_data->content_label[0]     = '\0';
   path_data->content_core_name[0] = '\0';
   path_data->content_db_name[0]   = '\0';
   path_data->content_img[0]       = '\0';
   path_data->content_img_full[0]  = '\0';
   path_data->content_img_short[0] = '\0';

   /* Must also reset playlist thumbnail display modes */
   path_data->playlist_right_mode  = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_left_mode   = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_index       = 0;

   if ((!img_dir || !*img_dir) || (!img_name || !*img_name))
      return false;

   if (path_is_media_type(img_name) != RARCH_CONTENT_IMAGE)
      return false;

   /* Cache content image name */
   strlcpy(path_data->content_img,
            img_name, sizeof(path_data->content_img));

   path_data->content_label_len = fill_pathname(
         path_data->content_label,
         path_data->content_img, "",
         sizeof(path_data->content_label));

   /* Set file path */
   fill_pathname_join_special(path_data->content_path,
      img_dir, img_name, sizeof(path_data->content_path));

   /* Set core name to "imageviewer" */
   strlcpy(path_data->content_core_name,
         "imageviewer",
         sizeof(path_data->content_core_name));

   /* Set database name (arbitrarily) to "_images_"
    * (required for compatibility with gfx_thumbnail_update_path(),
    * but not actually used...) */
   strlcpy(path_data->content_db_name,
         "_images_", sizeof(path_data->content_db_name));

   /* Redundant error check */
   return *path_data->content_path;
}

/* Sets current thumbnail content to the specified playlist entry.
 * Returns true if content is valid.
 * > Note: It is always best to use playlists when setting
 *   thumbnail content, since there is no guarantee that the
 *   corresponding menu entry label will contain a useful
 *   identifier (it may be 'tainted', e.g. with the current
 *   core name). 'Real' labels should be extracted from source */
bool gfx_thumbnail_set_content_playlist(
      gfx_thumbnail_path_data_t *path_data, playlist_t *playlist, size_t idx)
{
   const char *content_path           = NULL;
   const char *content_label          = NULL;
   const char *core_name              = NULL;
   const char *db_name                = NULL;
   const struct playlist_entry *entry = NULL;

   if (!path_data)
      return false;

   /* When content is updated, must regenerate right/left
    * thumbnail paths */
   path_data->right_path[0]           = '\0';
   path_data->left_path[0]            = '\0';

   /* 'Reset' path_data content strings */
   path_data->content_path[0]         = '\0';
   path_data->content_label_len       = 0;
   path_data->content_label[0]        = '\0';
   path_data->content_core_name[0]    = '\0';
   path_data->content_db_name[0]      = '\0';
   path_data->content_img[0]          = '\0';
   path_data->content_img_full[0]     = '\0';
   path_data->content_img_short[0]    = '\0';

   /* Must also reset playlist thumbnail display modes */
   path_data->playlist_right_mode     = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_left_mode      = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
   path_data->playlist_index          = 0;

   if (!playlist)
      return false;

   if (idx >= playlist_get_size(playlist))
      return false;

   /* Read playlist values */
   playlist_get_index(playlist, idx, &entry);

   if (!entry)
      return false;

   content_path  = entry->path;
   content_label = entry->label;
   core_name     = entry->core_name;
   db_name       = entry->db_name;

   /* Content without a path is invalid by definition */
   if (!content_path || !*content_path)
      return false;

   /* Cache content path
    * (This is required for imageviewer, history and favourites content) */
   strlcpy(path_data->content_path,
            content_path, sizeof(path_data->content_path));

   /* Cache core name
    * (This is required for imageviewer content) */
   if (core_name && *core_name)
      strlcpy(path_data->content_core_name,
            core_name, sizeof(path_data->content_core_name));

   /* Get content label */
   if (content_label && *content_label)
      path_data->content_label_len = strlcpy(path_data->content_label,
            content_label, sizeof(path_data->content_label));
   else
      path_data->content_label_len = fill_pathname(path_data->content_label,
            path_basename(content_path),
            "", sizeof(path_data->content_label));

   /* Determine content image name */
   {
      char tmp_buf[NAME_MAX_LENGTH];
      fill_pathname(tmp_buf, path_basename(path_data->content_path),
            "", sizeof(tmp_buf));

      gfx_thumbnail_fill_content_img(path_data->content_img_full,
         sizeof(path_data->content_img_full), tmp_buf, false);
      gfx_thumbnail_fill_content_img(path_data->content_img,
         sizeof(path_data->content_img), path_data->content_label, false);

      /* Explicit zero if full name is same as standard name 
         - saves some queries later. */
      if (string_is_equal(path_data->content_img,
          path_data->content_img_full))
         path_data->content_img_full[0] = '\0';

      gfx_thumbnail_fill_content_img(path_data->content_img_short,
         sizeof(path_data->content_img_short), path_data->content_label, true);
   }

   /* Store playlist index */
   path_data->playlist_index = idx;

   /* Redundant error check... */
   if (!*path_data->content_img)
      return false;

   /* Thumbnail image name is done -> now check if
    * per-content database name is defined */
   if (!db_name || !*db_name)
      playlist_get_db_name(playlist, idx, &db_name);
   if (db_name && *db_name)
   {
      /* Hack: There is only one MAME thumbnail repo,
       * so filter any input starting with 'MAME...' */
      if (strncmp(db_name, "MAME", 4) == 0)
      {
         path_data->content_db_name[0] = path_data->content_db_name[2] = 'M';
         path_data->content_db_name[1] = 'A';
         path_data->content_db_name[3] = 'E';
         path_data->content_db_name[4] = '\0';
      }
      else
      {
         char tmp_buf[NAME_MAX_LENGTH];
         const char *pos      = strchr(db_name, '|');
         /* If db_name comes from core info, and there are multiple
          * databases mentioned separated by |, use only first one */
         if (pos && (size_t) (pos - db_name) + 1 < sizeof(tmp_buf))
            strlcpy(tmp_buf, db_name, (size_t)(pos - db_name) + 1);
         else
            strlcpy(tmp_buf, db_name, sizeof(tmp_buf));

         fill_pathname(path_data->content_db_name,
               tmp_buf, "",
               sizeof(path_data->content_db_name));
      }
   }

   /* Playlist entry is valid -> it is now 'safe' to
    * extract any remaining playlist metadata
    * (i.e. thumbnail display modes) */
   path_data->playlist_right_mode =
         playlist_get_thumbnail_mode(playlist, PLAYLIST_THUMBNAIL_RIGHT);
   path_data->playlist_left_mode =
         playlist_get_thumbnail_mode(playlist, PLAYLIST_THUMBNAIL_LEFT);

   return true;
}

/* Updaters */

/* Updates path for specified thumbnail identifier (right, left).
 * Must be called after:
 * - gfx_thumbnail_set_system()
 * - gfx_thumbnail_set_content*()
 * ...and before:
 * - gfx_thumbnail_get_path()
 * Returns true if generated path is valid */
bool gfx_thumbnail_update_path(
      gfx_thumbnail_path_data_t *path_data,
      enum gfx_thumbnail_id thumbnail_id)
{
   char content_dir[DIR_MAX_LENGTH];
   settings_t *settings          = config_get_ptr();
   const char *system_name       = NULL;
   char *thumbnail_path          = NULL;
   const char *dir_thumbnails    = settings->paths.directory_thumbnails;
   bool playlist_allow_non_png   = settings->bools.playlist_allow_non_png;
   unsigned gfx_thumbnails       = settings->uints.gfx_thumbnails;
   unsigned menu_left_thumbnails = settings->uints.menu_left_thumbnails;
   unsigned menu_icon_thumbnails = settings->uints.menu_icon_thumbnails;
   /* Thumbnail extension order. The default (i.e. png) is always the first. */
   #define MAX_SUPPORTED_THUMBNAIL_EXTENSIONS 5
   const char* const SUPPORTED_THUMBNAIL_EXTENSIONS[] = { ".png", ".jpg", ".jpeg", ".bmp", ".tga", 0 };

   if (!path_data)
      return false;

   /* Determine which path we are updating... */
   switch (thumbnail_id)
   {
      case GFX_THUMBNAIL_RIGHT:
         thumbnail_path = path_data->right_path;
         break;
      case GFX_THUMBNAIL_LEFT:
         thumbnail_path = path_data->left_path;
         break;
      case GFX_THUMBNAIL_ICON:
         thumbnail_path = path_data->icon_path;
         break;
      default:
         return false;
   }

   content_dir[0]    = '\0';

   /* Sundry error checking */
   if (!dir_thumbnails || !*dir_thumbnails)
      return false;

   if (!gfx_thumbnail_is_enabled(path_data, thumbnail_id))
      return false;

   /* Generate new path */

   /* > Check path_data for empty strings */
   if (       (!*path_data->content_path)
       ||     (!*path_data->content_img)
       || (   (!*path_data->system)
           && (!*path_data->content_db_name)))
      return false;

   /* > Get current system */
   if (!*path_data->content_db_name)
   {
      /* If this is a content history or favorites playlist
       * then the current 'path_data->system' string is
       * meaningless. In this case, we fall back to the
       * content directory name */
      if (     memcmp(path_data->system, "history", 8) == 0
            || memcmp(path_data->system, "favorites", 10) == 0)
      {
         if (gfx_thumbnail_get_content_dir(
               path_data, content_dir, sizeof(content_dir)) == 0)
            return false;

         system_name = content_dir;
      }
      else
         system_name = path_data->system;
   }
   else
      system_name = path_data->content_db_name;

   /* > Special case: thumbnail for imageviewer content
    *   is the image file itself */
   if (   memcmp(system_name, "images_history", sizeof("images_history")) == 0
       || memcmp(path_data->content_core_name, "imageviewer", sizeof("imageviewer")) == 0)
   {
      /* imageviewer content is identical for left and right thumbnails */
      if (path_is_media_type(path_data->content_path) == RARCH_CONTENT_IMAGE)
         strlcpy(thumbnail_path,
               path_data->content_path, PATH_MAX_LENGTH * sizeof(char));
   }
   else
   {
      int  i;
      char tmp_buf[DIR_MAX_LENGTH];
      const char *type = gfx_thumbnail_get_type(gfx_thumbnails,
            menu_left_thumbnails, menu_icon_thumbnails, path_data, thumbnail_id);
      bool thumbnail_found = false;
      /* > Normal content: assemble path */

      /* >> Base + system name */
      fill_pathname_join_special(thumbnail_path, dir_thumbnails,
            system_name, PATH_MAX_LENGTH * sizeof(char));

      /* >> Add type */
      fill_pathname_join_special(tmp_buf, thumbnail_path, type, sizeof(tmp_buf));

      thumbnail_path[0] = '\0';

      /* >> Add content image - first try with label (database name) */
      if (*path_data->content_img)
      {
         fill_pathname_join_special(thumbnail_path, tmp_buf,
               path_data->content_img, PATH_MAX_LENGTH * sizeof(char));
         thumbnail_found = path_is_valid(thumbnail_path);

         if (playlist_allow_non_png && thumbnail_path && *thumbnail_path)
         {
            for (i = 1; i < MAX_SUPPORTED_THUMBNAIL_EXTENSIONS && !thumbnail_found; i++)
            {
               if (!path_get_extension_mutable(thumbnail_path))
                  continue;

               strlcpy(path_get_extension_mutable(thumbnail_path),
                     SUPPORTED_THUMBNAIL_EXTENSIONS[i], 6);
               thumbnail_found = path_is_valid(thumbnail_path);
            }
         }
      }

      /* >> Add content image - second try with full file name */
      if (   !thumbnail_found 
          && *path_data->content_img_full)
      {
         thumbnail_path[0] = '\0';
         fill_pathname_join_special(thumbnail_path, tmp_buf,
               path_data->content_img_full, PATH_MAX_LENGTH * sizeof(char));
         thumbnail_found = path_is_valid(thumbnail_path);

         if (playlist_allow_non_png && thumbnail_path && *thumbnail_path)
         {
            for (i = 1; i < MAX_SUPPORTED_THUMBNAIL_EXTENSIONS && !thumbnail_found; i++)
            {
               if (!path_get_extension_mutable(thumbnail_path))
                  continue;

               strlcpy(path_get_extension_mutable(thumbnail_path),
                     SUPPORTED_THUMBNAIL_EXTENSIONS[i], 6);
               thumbnail_found = path_is_valid(thumbnail_path);
            }
         }
      }

      /* >> Add content image - third try with shortened name (title only) */
      if (!thumbnail_found && *path_data->content_img_short)
      {
         thumbnail_path[0] = '\0';
         fill_pathname_join_special(thumbnail_path, tmp_buf,
               path_data->content_img_short, PATH_MAX_LENGTH * sizeof(char));
         thumbnail_found = path_is_valid(thumbnail_path);

         if (playlist_allow_non_png && thumbnail_path && *thumbnail_path)
         {
            for (i = 1; i < MAX_SUPPORTED_THUMBNAIL_EXTENSIONS && !thumbnail_found; i++)
            {
               if (!path_get_extension_mutable(thumbnail_path))
                  continue;

               strlcpy(path_get_extension_mutable(thumbnail_path),
                     SUPPORTED_THUMBNAIL_EXTENSIONS[i], 6);
               thumbnail_found = path_is_valid(thumbnail_path);
            }
         }
      }
      /* This logic is valid for locally stored thumbnails. For optional downloads,
       * gfx_thumbnail_get_img_name() is used */
   }

   /* Final error check - is cached path empty? */
   return thumbnail_path && *thumbnail_path;
}

/* Getters */

/* Fetches current content directory.
 * Returns true if content directory is valid. */
size_t gfx_thumbnail_get_content_dir(gfx_thumbnail_path_data_t *path_data,
      char *s, size_t len)
{
   size_t _len;
   char *last_slash;
   char tmp_buf[NAME_MAX_LENGTH];
   const char *dir_start;
   if (!path_data || !*path_data->content_path)
      return 0;
   if (!(last_slash = find_last_slash(path_data->content_path)))
      return 0;
   _len = last_slash + 1 - path_data->content_path;
   if (!((_len > 1) && (_len < PATH_MAX_LENGTH)))
      return 0;
   /* The historical implementation copied the whole directory
    * portion of content_path into tmp_buf and then took its
    * basename.  But content_path is sized PATH_MAX_LENGTH (2048)
    * and tmp_buf only NAME_MAX_LENGTH (256), so a directory
    * portion longer than 255 chars (reachable on systems with
    * deep folder hierarchies) caused strlcpy to write up to
    * _len-1 bytes into the 256-byte tmp_buf -- a stack overflow.
    *
    * Since the goal is the *basename* of the directory (i.e.
    * the segment between the second-to-last and last slashes),
    * skip the prefix copy: we only need the tail.  Anchor the
    * copy at the latest position that still lets the segment
    * fit in tmp_buf, then take its basename. */
   dir_start = path_data->content_path;
   if (_len > sizeof(tmp_buf))
      dir_start = last_slash + 1 - sizeof(tmp_buf);
   strlcpy(tmp_buf, dir_start,
         (last_slash - dir_start + 1) * sizeof(char));
   return strlcpy(s, path_basename_nocompression(tmp_buf), len);
}

/* Gets the common savestate thumbnail path. */
void gfx_savestate_thumbnail_get_path(
      char *s, size_t len,
      const char *state_name,
      int state_slot)
{
   size_t _len;
   playlist_t *playlist = playlist_get_cached();

   if (!s || !len)
      return;

   s[0] = '\0';

   if (!state_name || !*state_name)
      return;

#ifdef HAVE_MENU
   if (playlist)
   {
      struct menu_state *menu_st         = menu_state_get_ptr();
      runloop_state_t *runloop_st        = runloop_state_get_ptr();
      const struct playlist_entry *entry = NULL;

      if (menu_st && menu_st->driver_data)
         playlist_get_index(playlist, menu_st->driver_data->rpl_entry_selection_ptr, &entry);

      if (*entry->path)
      {
         size_t _len;
         char new_path[PATH_MAX_LENGTH];
         char entry_basename[PATH_MAX_LENGTH];

         strlcpy(new_path, entry->path, sizeof(new_path));
         path_remove_extension(new_path);

         _len = strlcpy(entry_basename, path_basename(new_path), sizeof(entry_basename));

         strlcpy(entry_basename + _len, ".state", sizeof(entry_basename) - _len);
         fill_pathname_join_special(new_path,
               runloop_st->savestate_dir, entry_basename, sizeof(new_path));

         state_name = strdup(new_path);
      }
   }
#endif /* HAVE_MENU */

   _len = strlcpy(s, state_name, len);

   /* The historical implementation accumulated _len from
    * strlcpy / snprintf returns and used `len - _len` as the
    * size for subsequent calls.  That pattern is NOT
    * self-bounding: strlcpy returns strlen(@src), and snprintf
    * returns the would-be length on truncation, so on any
    * truncating call _len overshoots @len, the next len-_len
    * subtraction underflows size_t to ~SIZE_MAX, and the
    * subsequent strlcpy treats the destination as essentially
    * infinite.  Reachable when state_name approaches PATH_MAX_LENGTH
    * (e.g. content loaded from a deep directory tree, since
    * runloop_st->name.savestate is sized PATH_MAX_LENGTH) and
    * the slot suffix or extension push the total past @len.
    *
    * Clamp _len after each truncation so the chain stays inside
    * the buffer instead of running off the end. */
   if (_len >= len)
      _len = len - 1;

   if (state_slot > 0)
   {
      int n = snprintf(s + _len, len - _len, "%d", state_slot);
      if (n < 0)
         return;
      _len += (size_t)n;
      if (_len >= len)
         _len = len - 1;
   }
   else if (state_slot < 0)
   {
      _len = fill_pathname_join_delim(s, state_name, "auto", '.', len);
      if (_len >= len)
         _len = len - 1;
   }

   strlcpy(s + _len, FILE_PATH_PNG_EXTENSION, len - _len);
}
