thumbnail_cache.vala 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /*
  2. * Copyright (c) 2012-2025 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public class ThumbnailCache
  8. {
  9. [Compact]
  10. public struct CacheEntry
  11. {
  12. int id; ///< Entry unique ID. Used to convert the entry to a Pixbuf area inside the atlas.
  13. unowned List<StringId64?> lru; ///< Pointer to LRU list entry.
  14. uint64 mtime; ///< Pixbuf last modification time.
  15. bool pending; ///< Whether a request to generate the thumbnail is pending.
  16. }
  17. public const int THUMBNAIL_SIZE = 64;
  18. public Project _project;
  19. public RuntimeInstance _thumbnail;
  20. public Gdk.Pixbuf _atlas;
  21. public int _mip0_width;
  22. public int _mip0_height;
  23. public GLib.List<StringId64?> _list;
  24. public Gee.HashMap<StringId64?, CacheEntry?> _map;
  25. public uint _max_cache_size;
  26. public bool _no_disk_cache; // Debug only: always go through server to get a thumbnail.
  27. public PixbufView _debug_pixbuf;
  28. public Gtk.Window _debug_window;
  29. // Called when the cache changed its content.
  30. public signal void changed();
  31. public ThumbnailCache(Project project, RuntimeInstance thumbnail, uint max_cache_size)
  32. {
  33. _project = project;
  34. _thumbnail = thumbnail;
  35. _list = new GLib.List<StringId64?>();
  36. _map = new Gee.HashMap<StringId64?, CacheEntry?>(StringId64.hash_func, StringId64.equal_func);
  37. _no_disk_cache = false;
  38. reset(max_cache_size);
  39. }
  40. public string thumbnail_path(string resource_path)
  41. {
  42. GLib.File file = GLib.File.new_for_path(_project.absolute_path(resource_path));
  43. uint8 uri_md5[16];
  44. Md5.State st = Md5.State();
  45. st.append(file.get_uri().data);
  46. st.finish(out uri_md5);
  47. string thumb_filename = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x.png".printf(uri_md5[0]
  48. , uri_md5[1]
  49. , uri_md5[2]
  50. , uri_md5[3]
  51. , uri_md5[4]
  52. , uri_md5[5]
  53. , uri_md5[6]
  54. , uri_md5[7]
  55. , uri_md5[8]
  56. , uri_md5[9]
  57. , uri_md5[10]
  58. , uri_md5[11]
  59. , uri_md5[12]
  60. , uri_md5[13]
  61. , uri_md5[14]
  62. , uri_md5[15]
  63. );
  64. return GLib.Path.build_filename(_thumbnails_normal_dir.get_path(), thumb_filename);
  65. }
  66. public Gdk.Pixbuf? thumbnail_subpixbuf(int entry_id, int thumb_size = THUMBNAIL_SIZE)
  67. {
  68. assert(thumb_size <= THUMBNAIL_SIZE);
  69. int thumbs_per_row = _mip0_width / THUMBNAIL_SIZE;
  70. int thumb_row = entry_id / thumbs_per_row;
  71. int thumb_col = entry_id % thumbs_per_row;
  72. int mip_level = THUMBNAIL_SIZE / thumb_size;
  73. int dest_x;
  74. int dest_y;
  75. if (mip_level == 1) {
  76. dest_x = thumb_col * thumb_size;
  77. dest_y = thumb_row * thumb_size;
  78. } else {
  79. int half_ml = mip_level / 2;
  80. dest_x = thumb_col * thumb_size + _mip0_width;
  81. dest_y = thumb_row * thumb_size + (int)(_mip0_height * ((half_ml - 1) / (double)half_ml));
  82. }
  83. return new Gdk.Pixbuf.subpixbuf(_atlas
  84. , dest_x
  85. , dest_y
  86. , thumb_size
  87. , thumb_size
  88. );
  89. }
  90. public void thumbnail_ready(string type, string name, string thumb_path)
  91. {
  92. string resource_path = ResourceId.path(type, name);
  93. StringId64 resource_id = StringId64(resource_path);
  94. // Rename thumb_path to destination atomically.
  95. GLib.File thumb_path_tmp = GLib.File.new_for_path(thumb_path);
  96. GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
  97. try {
  98. thumb_path_tmp.move(thumb_path_dst, GLib.FileCopyFlags.OVERWRITE);
  99. } catch (GLib.Error e) {
  100. loge(e.message);
  101. }
  102. CacheEntry? entry = _map.get(resource_id);
  103. if (entry == null)
  104. return;
  105. try {
  106. // Read thumbnail from disk.
  107. uint64 thumb_mtime = 0;
  108. GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  109. GLib.DateTime? mdate = thumb_info.get_modification_date_time();
  110. if (mdate != null)
  111. thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
  112. load_thumbnail_from_path(entry.id, thumb_path_dst.get_path());
  113. entry.mtime = thumb_mtime;
  114. entry.pending = false;
  115. _map.set(resource_id, entry);
  116. assert(_map.get(resource_id).mtime == thumb_mtime);
  117. } catch (GLib.Error e) {
  118. loge(e.message);
  119. }
  120. }
  121. // Copies @a thumbnail inside the atlas at the position defined by @a subpixbuf.
  122. public void copy_thumbnail(Gdk.Pixbuf? subpixbuf, Gdk.Pixbuf? thumbnail)
  123. {
  124. thumbnail.copy_area(0
  125. , 0
  126. , thumbnail.width
  127. , thumbnail.height
  128. , subpixbuf
  129. , 0
  130. , 0
  131. );
  132. changed();
  133. }
  134. // Generates mips for @a thumb_id. The main thumbnail for @a thumb_id
  135. // is assumed to be already loaded into the atlas.
  136. public void generate_mips(int thumb_id)
  137. {
  138. Gdk.Pixbuf main_thumbnail = thumbnail_subpixbuf(thumb_id, THUMBNAIL_SIZE);
  139. for (int size = THUMBNAIL_SIZE / 2; size >= 16; size /= 2) {
  140. Gdk.Pixbuf? mip = thumbnail_subpixbuf(thumb_id, size);
  141. Gdk.Pixbuf main_scaled = main_thumbnail.scale_simple(mip.width, mip.height, Gdk.InterpType.BILINEAR);
  142. copy_thumbnail(mip, main_scaled);
  143. }
  144. }
  145. public void load_thumbnail_from_path(int thumb_id, string thumbnail_path) throws GLib.Error
  146. {
  147. Gdk.Pixbuf? subpixbuf = thumbnail_subpixbuf(thumb_id, THUMBNAIL_SIZE);
  148. try {
  149. var thumbnail = new Gdk.Pixbuf.from_file_at_size(thumbnail_path
  150. , subpixbuf.width
  151. , subpixbuf.height
  152. );
  153. copy_thumbnail(subpixbuf, thumbnail);
  154. generate_mips(thumb_id);
  155. } finally {
  156. // Empty.
  157. }
  158. }
  159. public void reset(uint max_size)
  160. {
  161. _list = new GLib.List<StringId64?>(); // No clear?
  162. _map.clear();
  163. double mip0_max_area = (double)max_size * (2.0/3.0); // Remaining 1/3 for mip 1, 2, ...
  164. _mip0_width = (int)(Math.sqrt(mip0_max_area));
  165. _mip0_width /= 4; // 4 bytes per pixel.
  166. _mip0_width -= _mip0_width % THUMBNAIL_SIZE;
  167. _mip0_height = _mip0_width;
  168. _max_cache_size = (_mip0_width / THUMBNAIL_SIZE) * (_mip0_height / THUMBNAIL_SIZE);
  169. _atlas = new Gdk.Pixbuf(Gdk.Colorspace.RGB
  170. , true
  171. , 8
  172. , _mip0_width + _mip0_width / 2
  173. , _mip0_height
  174. );
  175. changed();
  176. }
  177. public Gdk.Pixbuf? get(string type, string name, int thumb_size = THUMBNAIL_SIZE)
  178. {
  179. if (!_project.is_loaded())
  180. return null;
  181. string resource_path = ResourceId.path(type, name);
  182. StringId64 resource_id = StringId64(resource_path);
  183. CacheEntry? entry = null;
  184. // Allocate a subpixbuf slot inside the atlas.
  185. if (_map.has_key(resource_id)) {
  186. entry = _map.get(resource_id);
  187. // Set resource_id as most recently used entry.
  188. _list.remove_link(entry.lru);
  189. _list.append(resource_id);
  190. entry.lru = _list.last();
  191. _map.set(resource_id, entry);
  192. } else {
  193. int entry_id = 0;
  194. if (_list.length() == _max_cache_size) {
  195. // Evict the least recently used entry.
  196. unowned List<StringId64?> lru = _list.nth(0);
  197. // Reuse the subpixbuf from the evicted entry.
  198. entry_id = _map.get(lru.data).id;
  199. _map.unset(lru.data);
  200. _list.remove_link(lru);
  201. } else {
  202. // Create a new subpixbuf if the entry is new.
  203. entry_id = (int)_list.length();
  204. }
  205. uint64 thumb_mtime = 0;
  206. try {
  207. if (!_no_disk_cache) {
  208. // Read thumbnail from disk.
  209. GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
  210. GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  211. GLib.DateTime? mdate = thumb_info.get_modification_date_time();
  212. if (mdate != null)
  213. thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
  214. load_thumbnail_from_path(entry_id, thumb_path_dst.get_path());
  215. }
  216. } catch (GLib.Error e) {
  217. // Nobody cares.
  218. }
  219. // Create a new cache entry.
  220. _list.append(resource_id);
  221. entry = { entry_id, _list.last(), thumb_mtime, false };
  222. _map.set(resource_id, entry);
  223. }
  224. if (!entry.pending && (entry.mtime == 0 || entry.mtime <= _project.mtime(type, name))) {
  225. // On-disk thumbnail not found or outdated.
  226. // Ask the server to generate a fresh one if the data is ready.
  227. if (_project._data_compiled) {
  228. try {
  229. // Create a unique temporary file to store the thumbnail's data.
  230. FileIOStream fs;
  231. GLib.File thumb_path_tmp = GLib.File.new_tmp(null, out fs);
  232. fs.close();
  233. // Request a new thumbnail.
  234. entry.pending = true;
  235. _map.set(resource_id, entry);
  236. _thumbnail.send_script(ThumbnailApi.add_request(type, name, thumb_path_tmp.get_path()));
  237. _thumbnail.send(DeviceApi.frame());
  238. } catch (GLib.Error e) {
  239. loge(e.message);
  240. }
  241. }
  242. }
  243. return thumbnail_subpixbuf(entry.id, thumb_size);
  244. }
  245. public void show_debug_window(Gtk.Window? parent_window)
  246. {
  247. if (_debug_window == null) {
  248. _debug_pixbuf = new PixbufView();
  249. _debug_pixbuf._zoom = 0.4;
  250. _debug_window = new Gtk.Window();
  251. _debug_window.set_title("ThumbnailCache Debug");
  252. _debug_window.set_size_request(800, 800);
  253. _debug_window.add(_debug_pixbuf);
  254. this.changed.connect(() => {
  255. _debug_pixbuf.set_pixbuf(_atlas);
  256. _debug_pixbuf.queue_draw();
  257. });
  258. }
  259. _debug_window.set_transient_for(parent_window);
  260. _debug_window.show_all();
  261. _debug_window.present();
  262. }
  263. }
  264. } /* namespace Crown */