From 48bf0f909f73ca031b0da4f6abed2765cd032aa8 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Wed, 7 Jan 2026 12:45:38 -0500 Subject: [PATCH 1/6] mipmap_cache: respect conf request to always use embedded thumbnail When conf "plugins/lighttable/thumbnail_raw_min_level" is "never", always use the thumbnail embedded in a raw, even when it is lower resolution the requested mipmap. This fixes a problem particularly visible on high dpi screens with fractional scaling: Due to limitations of GTK3 (which always returns a whole-number from gtk_widget_get_scale_factor), we generate mipmaps larger than the physical display resolution. These can easily exceed the resolution of the embedded JPEG. Fixes #19944. --- src/common/mipmap_cache.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/mipmap_cache.c b/src/common/mipmap_cache.c index 19cc83122ca4..f5f013932ce1 100644 --- a/src/common/mipmap_cache.c +++ b/src/common/mipmap_cache.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2011-2025 darktable developers. + Copyright (C) 2011-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -1504,16 +1504,16 @@ static void _init_8(uint8_t *buf, res = dt_imageio_large_thumbnail(filename, &tmp, &thumb_width, &thumb_height, color_space); if(!res) { - // if the thumbnail is not large enough, we compute one + // use embedded JPEG if it is large enough or conf requests + // always use, otherwise compute one const dt_image_t *img2 = dt_image_cache_get(imgid, 'r'); const int imgwd = img2->width; const int imght = img2->height; dt_image_cache_read_release(img2); - if(thumb_width < wd - && thumb_height < ht - && thumb_width < imgwd - 4 - && thumb_height < imght - 4) - { + const gboolean always_use_thumb = (min_s == DT_MIPMAP_NONE); + const gboolean thumb_lt_mip = ((thumb_width < wd) && (thumb_height < ht)); + const gboolean thumb_lt_raw = ((thumb_width < imgwd - 4) && (thumb_height < imght - 4)); + if (!always_use_thumb && thumb_lt_mip && thumb_lt_raw) { res = TRUE; } else From d76f176d6900eab4c9adab2b3a2cf41a2e51e0fa Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Wed, 7 Jan 2026 14:34:29 -0500 Subject: [PATCH 2/6] culling: take into account display scaling in thumbs prefetch When prefetching an image, take into account scaling from logical to device pixels when determining requested mipmap size. Prior to this commit, when display scaling > 100%, dt may prefetch a too small mipmap. This results in seeing a "working..." message when moving through images (via preview or normal culling mode), with the initially displayed mipmap soon after replaced by a slightly higher resolution mipmap. --- src/dtgtk/culling.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dtgtk/culling.c b/src/dtgtk/culling.c index e3e77ccf1225..6ff6e3a323d1 100644 --- a/src/dtgtk/culling.c +++ b/src/dtgtk/culling.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2020-2023 darktable developers. + Copyright (C) 2020-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -1155,8 +1155,10 @@ static void _thumbs_prefetch(dt_culling_t *table) maxw = MAX(maxw, th->width); maxh = MAX(maxh, th->height); } + const int32_t mipwidth = maxw * darktable.gui->ppd; + const int32_t mipheight = maxh * darktable.gui->ppd; dt_mipmap_size_t mip = - dt_mipmap_cache_get_matching_size(maxw, maxh); + dt_mipmap_cache_get_matching_size(mipwidth, mipheight); // prefetch next image gchar *query; From 38ca97ec38c4ef757cf27164c090baa809d6b68f Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Thu, 8 Jan 2026 22:57:04 -0500 Subject: [PATCH 3/6] add "auto" behavior for generating mipmaps These replicate the prior behavior of "never" mipmaps for conf "plugins/lighttable/thumbnail_raw_min_level": When generating a mipmap from an altered image, use the embedded JPEG unless the requested mipmap size is greater than the embedded JPEG size. In that case generate the image from running the full pixelpipe. This allows for fast mipmaps, but unlike the "never" behavior, avoids upscaling the embedded JPEG Update the thumbnail discard code to handle switching between auto/never modes. We don't have the metadata for mipmaps to know whether they were/weren't upscaled or generated from a JPEG/RAW. Rather than modify mipmap code to support an edge case, use a heuristic: All semi-recent cameras appear to have embedded JPEG of at least mip3 quality. So on switch between auto/never modes, discarding all mip4 or greater resoluton is sufficient. This code also does the right thing when switching between "always", "small", through "5K" values and "never"/"auto" without needing special case handling. --- data/darktableconfig.xml.in | 7 ++++--- src/common/mipmap_cache.c | 1 + src/dtgtk/thumbtable.c | 19 ++++++++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index 5cfe9440cb32..e554e34b492c 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -1332,7 +1332,7 @@ plugins/lighttable/thumbnail_raw_min_level - + @@ -1343,11 +1343,12 @@ + never - use raw file instead of embedded JPEG from size - if the thumbnail size is greater than this value, it will be processed using raw file instead of the embedded preview JPEG (better but slower).\nif you want all thumbnails and pre-rendered images in best quality you should choose the *always* option.\n(more comments in the manual) + for unaltered images, use raw file instead of embedded JPEG from size + if the thumbnail size is greater than this value, it will be processed using raw file instead of the embedded preview JPEG (better but slower).\nif you want all thumbnails and pre-rendered images in best quality you should choose the *always* option.\nfor the quickest display, choose the *never* option\nthe *auto* option prefers the embedded JPEG except when the when thumb size exceeds the resolution of the embedded JPEG\n(more comments in the manual) plugins/lighttable/thumbnail_hq_min_level diff --git a/src/common/mipmap_cache.c b/src/common/mipmap_cache.c index f5f013932ce1..c1667dc4d050 100644 --- a/src/common/mipmap_cache.c +++ b/src/common/mipmap_cache.c @@ -1215,6 +1215,7 @@ dt_mipmap_size_t dt_mipmap_cache_get_min_mip_from_pref(const char *value) if(strcmp(value, "WQXGA") == 0) return DT_MIPMAP_5; if(strcmp(value, "4K") == 0) return DT_MIPMAP_6; if(strcmp(value, "5K") == 0) return DT_MIPMAP_7; + if(strcmp(value, "auto") == 0) return DT_MIPMAP_8; return DT_MIPMAP_NONE; } diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index 83ee83d15ca3..43f4dd133ba3 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -1669,13 +1669,13 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) dt_conf_get_string_const("plugins/lighttable/thumbnail_hq_min_level"); dt_mipmap_size_t hql = dt_mipmap_cache_get_min_mip_from_pref(hq); + const char *embedded = dt_conf_get_string_const("plugins/lighttable/thumbnail_raw_min_level"); - dt_mipmap_size_t embeddedl = dt_mipmap_cache_get_min_mip_from_pref(embedded); - int min_level = 8; - int max_level = 0; + int min_level = DT_MIPMAP_8; + int max_level = DT_MIPMAP_0; if(hql != table->pref_hq) { min_level = MIN(table->pref_hq, hql); @@ -1696,6 +1696,10 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) " how thumbnails are generated.\n")); if(max_level >= DT_MIPMAP_8 && min_level == DT_MIPMAP_0) dt_util_str_cat(&txt, _("all cached thumbnails need to be invalidated.\n\n")); + else if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8 && embeddedl == DT_MIPMAP_NONE) + dt_util_str_cat(&txt, _("all thumbnails possibly generated from raw files need to be invalidated.\n\n")); + else if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8 && embeddedl == DT_MIPMAP_8) + dt_util_str_cat(&txt, _("all thumbnails possibly upscaled from embedded JPEGs need to be invalidated.\n\n")); else if(max_level >= DT_MIPMAP_8) dt_util_str_cat (&txt, @@ -1717,6 +1721,15 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) if(dt_gui_show_yes_no_dialog(_("cached thumbnails invalidation"), "", "%s", txt)) { + // switching between auto/never modes + if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8) + { + // err on side of discarding too many thumbnails: a quick + // survey of vintage raw files shows a lowest res embedded + // JPEG of 1616x1080 (found in 2011 & 2014 Sony) + min_level = DT_MIPMAP_4; + } + DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT id FROM main.images", -1, &stmt, NULL); while(sqlite3_step(stmt) == SQLITE_ROW) From 1a7b5f9e735fdf8e8e0c50877aeed099705659b4 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Fri, 9 Jan 2026 10:36:36 -0500 Subject: [PATCH 4/6] thumbtable: properly discard highest-res mipmaps The discard loop previously skipped the highest resolution mipmap in the range. Prior behavior has been in the code since it was committed in 4f0b66878529c429d419cde580e764d9d05bac61 but looks to be an omission. --- src/dtgtk/thumbtable.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index 43f4dd133ba3..67d8e53ea749 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -1735,7 +1735,7 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) while(sqlite3_step(stmt) == SQLITE_ROW) { const dt_imgid_t imgid = sqlite3_column_int(stmt, 0); - for(int i = max_level - 1; i >= min_level; i--) + for(int i = max_level; i >= min_level; i--) { dt_mipmap_cache_remove_at_size(imgid, i); } From e6847b1ac96021c8728fa9175f48ec81bd7ec510 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Fri, 9 Jan 2026 11:03:44 -0500 Subject: [PATCH 5/6] add release note for mipmap generation bufix --- RELEASE_NOTES.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 287c3f8f1ba9..8d2bf2a6afb6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -65,7 +65,12 @@ changes (where available). ## Bug Fixes -- N/A +- Honor the default configuration preference "never" for "use raw + instead of jpeg from size": for unaltered images, always generate + thumbnails/previews from embedded JPEGs rather than processing the + raw file. If you prefer the prior behavior, which processed the raw + file rather than upscale the embedded JPEG for higher resolution + thumbnails/previews, use the new configuration option "auto". ## Lua From 699b43f943e154f3ddae74f7e8556e056a2e19b9 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Fri, 9 Jan 2026 12:28:14 -0500 Subject: [PATCH 6/6] thumbtable: simplify the discard code for auto/never Don't need to have bespoke messages for auto/never conf options. --- src/dtgtk/thumbtable.c | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index 67d8e53ea749..142ee0d1bd9d 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -1687,6 +1687,15 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) max_level = MAX(max_level, MAX(table->pref_embedded, embeddedl)); } + // switching between auto/never options + if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8) + { + // err on side of discarding too many thumbnails: a quick + // survey of vintage raw files shows a lowest res embedded + // JPEG of 1616x1080 (found in 2011 & 2014 Sony) + min_level = DT_MIPMAP_4; + } + sqlite3_stmt *stmt = NULL; if(min_level < max_level) @@ -1696,10 +1705,6 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) " how thumbnails are generated.\n")); if(max_level >= DT_MIPMAP_8 && min_level == DT_MIPMAP_0) dt_util_str_cat(&txt, _("all cached thumbnails need to be invalidated.\n\n")); - else if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8 && embeddedl == DT_MIPMAP_NONE) - dt_util_str_cat(&txt, _("all thumbnails possibly generated from raw files need to be invalidated.\n\n")); - else if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8 && embeddedl == DT_MIPMAP_8) - dt_util_str_cat(&txt, _("all thumbnails possibly upscaled from embedded JPEGs need to be invalidated.\n\n")); else if(max_level >= DT_MIPMAP_8) dt_util_str_cat (&txt, @@ -1721,15 +1726,6 @@ static void _thumbs_ask_for_discard(dt_thumbtable_t *table) if(dt_gui_show_yes_no_dialog(_("cached thumbnails invalidation"), "", "%s", txt)) { - // switching between auto/never modes - if (max_level == DT_MIPMAP_NONE && min_level == DT_MIPMAP_8) - { - // err on side of discarding too many thumbnails: a quick - // survey of vintage raw files shows a lowest res embedded - // JPEG of 1616x1080 (found in 2011 & 2014 Sony) - min_level = DT_MIPMAP_4; - } - DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT id FROM main.images", -1, &stmt, NULL); while(sqlite3_step(stmt) == SQLITE_ROW)