| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- /*
- * Copyright (c) 2012-2025 Daniele Bartolini et al.
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
- namespace Crown
- {
- public class ThumbnailCache
- {
- [Compact]
- public struct CacheEntry
- {
- int id; ///< Entry unique ID. Used to convert the entry to a Pixbuf area inside the atlas.
- unowned List<StringId64?> lru; ///< Pointer to LRU list entry.
- uint64 mtime; ///< Pixbuf last modification time.
- bool pending; ///< Whether a request to generate the thumbnail is pending.
- }
- public const int THUMBNAIL_SIZE = 64;
- public Project _project;
- public RuntimeInstance _thumbnail;
- public Gdk.Pixbuf _atlas;
- public int _mip0_width;
- public int _mip0_height;
- public GLib.List<StringId64?> _list;
- public Gee.HashMap<StringId64?, CacheEntry?> _map;
- public uint _max_cache_size;
- public bool _no_disk_cache; // Debug only: always go through server to get a thumbnail.
- public PixbufView _debug_pixbuf;
- public Gtk.Window _debug_window;
- // Called when the cache changed its content.
- public signal void changed();
- public ThumbnailCache(Project project, RuntimeInstance thumbnail, uint max_cache_size)
- {
- _project = project;
- _thumbnail = thumbnail;
- _list = new GLib.List<StringId64?>();
- _map = new Gee.HashMap<StringId64?, CacheEntry?>(StringId64.hash_func, StringId64.equal_func);
- _no_disk_cache = false;
- reset(max_cache_size);
- }
- public string thumbnail_path(string resource_path)
- {
- GLib.File file = GLib.File.new_for_path(_project.absolute_path(resource_path));
- uint8 uri_md5[16];
- Md5.State st = Md5.State();
- st.append(file.get_uri().data);
- st.finish(out uri_md5);
- string thumb_filename = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x.png".printf(uri_md5[0]
- , uri_md5[1]
- , uri_md5[2]
- , uri_md5[3]
- , uri_md5[4]
- , uri_md5[5]
- , uri_md5[6]
- , uri_md5[7]
- , uri_md5[8]
- , uri_md5[9]
- , uri_md5[10]
- , uri_md5[11]
- , uri_md5[12]
- , uri_md5[13]
- , uri_md5[14]
- , uri_md5[15]
- );
- return GLib.Path.build_filename(_thumbnails_normal_dir.get_path(), thumb_filename);
- }
- public Gdk.Pixbuf? thumbnail_subpixbuf(int entry_id, int thumb_size = THUMBNAIL_SIZE)
- {
- assert(thumb_size <= THUMBNAIL_SIZE);
- int thumbs_per_row = _mip0_width / THUMBNAIL_SIZE;
- int thumb_row = entry_id / thumbs_per_row;
- int thumb_col = entry_id % thumbs_per_row;
- int mip_level = THUMBNAIL_SIZE / thumb_size;
- int dest_x;
- int dest_y;
- if (mip_level == 1) {
- dest_x = thumb_col * thumb_size;
- dest_y = thumb_row * thumb_size;
- } else {
- int half_ml = mip_level / 2;
- dest_x = thumb_col * thumb_size + _mip0_width;
- dest_y = thumb_row * thumb_size + (int)(_mip0_height * ((half_ml - 1) / (double)half_ml));
- }
- return new Gdk.Pixbuf.subpixbuf(_atlas
- , dest_x
- , dest_y
- , thumb_size
- , thumb_size
- );
- }
- public void thumbnail_ready(string type, string name, string thumb_path)
- {
- string resource_path = ResourceId.path(type, name);
- StringId64 resource_id = StringId64(resource_path);
- // Rename thumb_path to destination atomically.
- GLib.File thumb_path_tmp = GLib.File.new_for_path(thumb_path);
- GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
- try {
- thumb_path_tmp.move(thumb_path_dst, GLib.FileCopyFlags.OVERWRITE);
- } catch (GLib.Error e) {
- loge(e.message);
- }
- CacheEntry? entry = _map.get(resource_id);
- if (entry == null)
- return;
- try {
- // Read thumbnail from disk.
- uint64 thumb_mtime = 0;
- GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
- GLib.DateTime? mdate = thumb_info.get_modification_date_time();
- if (mdate != null)
- thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
- load_thumbnail_from_path(entry.id, thumb_path_dst.get_path());
- entry.mtime = thumb_mtime;
- entry.pending = false;
- _map.set(resource_id, entry);
- assert(_map.get(resource_id).mtime == thumb_mtime);
- } catch (GLib.Error e) {
- loge(e.message);
- }
- }
- // Copies @a thumbnail inside the atlas at the position defined by @a subpixbuf.
- public void copy_thumbnail(Gdk.Pixbuf? subpixbuf, Gdk.Pixbuf? thumbnail)
- {
- thumbnail.copy_area(0
- , 0
- , thumbnail.width
- , thumbnail.height
- , subpixbuf
- , 0
- , 0
- );
- changed();
- }
- // Generates mips for @a thumb_id. The main thumbnail for @a thumb_id
- // is assumed to be already loaded into the atlas.
- public void generate_mips(int thumb_id)
- {
- Gdk.Pixbuf main_thumbnail = thumbnail_subpixbuf(thumb_id, THUMBNAIL_SIZE);
- for (int size = THUMBNAIL_SIZE / 2; size >= 16; size /= 2) {
- Gdk.Pixbuf? mip = thumbnail_subpixbuf(thumb_id, size);
- Gdk.Pixbuf main_scaled = main_thumbnail.scale_simple(mip.width, mip.height, Gdk.InterpType.BILINEAR);
- copy_thumbnail(mip, main_scaled);
- }
- }
- public void load_thumbnail_from_path(int thumb_id, string thumbnail_path) throws GLib.Error
- {
- Gdk.Pixbuf? subpixbuf = thumbnail_subpixbuf(thumb_id, THUMBNAIL_SIZE);
- try {
- var thumbnail = new Gdk.Pixbuf.from_file_at_size(thumbnail_path
- , subpixbuf.width
- , subpixbuf.height
- );
- copy_thumbnail(subpixbuf, thumbnail);
- generate_mips(thumb_id);
- } finally {
- // Empty.
- }
- }
- public void reset(uint max_size)
- {
- _list = new GLib.List<StringId64?>(); // No clear?
- _map.clear();
- double mip0_max_area = (double)max_size * (2.0/3.0); // Remaining 1/3 for mip 1, 2, ...
- _mip0_width = (int)(Math.sqrt(mip0_max_area));
- _mip0_width /= 4; // 4 bytes per pixel.
- _mip0_width -= _mip0_width % THUMBNAIL_SIZE;
- _mip0_height = _mip0_width;
- _max_cache_size = (_mip0_width / THUMBNAIL_SIZE) * (_mip0_height / THUMBNAIL_SIZE);
- _atlas = new Gdk.Pixbuf(Gdk.Colorspace.RGB
- , true
- , 8
- , _mip0_width + _mip0_width / 2
- , _mip0_height
- );
- changed();
- }
- public Gdk.Pixbuf? get(string type, string name, int thumb_size = THUMBNAIL_SIZE)
- {
- if (!_project.is_loaded())
- return null;
- string resource_path = ResourceId.path(type, name);
- StringId64 resource_id = StringId64(resource_path);
- CacheEntry? entry = null;
- // Allocate a subpixbuf slot inside the atlas.
- if (_map.has_key(resource_id)) {
- entry = _map.get(resource_id);
- // Set resource_id as most recently used entry.
- _list.remove_link(entry.lru);
- _list.append(resource_id);
- entry.lru = _list.last();
- _map.set(resource_id, entry);
- } else {
- int entry_id = 0;
- if (_list.length() == _max_cache_size) {
- // Evict the least recently used entry.
- unowned List<StringId64?> lru = _list.nth(0);
- // Reuse the subpixbuf from the evicted entry.
- entry_id = _map.get(lru.data).id;
- _map.unset(lru.data);
- _list.remove_link(lru);
- } else {
- // Create a new subpixbuf if the entry is new.
- entry_id = (int)_list.length();
- }
- uint64 thumb_mtime = 0;
- try {
- if (!_no_disk_cache) {
- // Read thumbnail from disk.
- GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
- GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
- GLib.DateTime? mdate = thumb_info.get_modification_date_time();
- if (mdate != null)
- thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
- load_thumbnail_from_path(entry_id, thumb_path_dst.get_path());
- }
- } catch (GLib.Error e) {
- // Nobody cares.
- }
- // Create a new cache entry.
- _list.append(resource_id);
- entry = { entry_id, _list.last(), thumb_mtime, false };
- _map.set(resource_id, entry);
- }
- if (!entry.pending && (entry.mtime == 0 || entry.mtime <= _project.mtime(type, name))) {
- // On-disk thumbnail not found or outdated.
- // Ask the server to generate a fresh one if the data is ready.
- if (_project._data_compiled) {
- try {
- // Create a unique temporary file to store the thumbnail's data.
- FileIOStream fs;
- GLib.File thumb_path_tmp = GLib.File.new_tmp(null, out fs);
- fs.close();
- // Request a new thumbnail.
- entry.pending = true;
- _map.set(resource_id, entry);
- _thumbnail.send_script(ThumbnailApi.add_request(type, name, thumb_path_tmp.get_path()));
- _thumbnail.send(DeviceApi.frame());
- } catch (GLib.Error e) {
- loge(e.message);
- }
- }
- }
- return thumbnail_subpixbuf(entry.id, thumb_size);
- }
- public void show_debug_window(Gtk.Window? parent_window)
- {
- if (_debug_window == null) {
- _debug_pixbuf = new PixbufView();
- _debug_pixbuf._zoom = 0.4;
- _debug_window = new Gtk.Window();
- _debug_window.set_title("ThumbnailCache Debug");
- _debug_window.set_size_request(800, 800);
- _debug_window.add(_debug_pixbuf);
- this.changed.connect(() => {
- _debug_pixbuf.set_pixbuf(_atlas);
- _debug_pixbuf.queue_draw();
- });
- }
- _debug_window.set_transient_for(parent_window);
- _debug_window.show_all();
- _debug_window.present();
- }
- }
- } /* namespace Crown */
|