1/**************************************************************************/
2/* editor_resource_preview.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_resource_preview.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/file_access.h"
35#include "core/io/resource_loader.h"
36#include "core/io/resource_saver.h"
37#include "core/object/message_queue.h"
38#include "core/variant/variant_utility.h"
39#include "editor/editor_node.h"
40#include "editor/editor_paths.h"
41#include "editor/editor_scale.h"
42#include "editor/editor_settings.h"
43#include "editor/editor_string_names.h"
44#include "scene/resources/image_texture.h"
45
46bool EditorResourcePreviewGenerator::handles(const String &p_type) const {
47 bool success = false;
48 if (GDVIRTUAL_CALL(_handles, p_type, success)) {
49 return success;
50 }
51 ERR_FAIL_V_MSG(false, "EditorResourcePreviewGenerator::_handles needs to be overridden.");
52}
53
54Ref<Texture2D> EditorResourcePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {
55 Ref<Texture2D> preview;
56 if (GDVIRTUAL_CALL(_generate, p_from, p_size, p_metadata, preview)) {
57 return preview;
58 }
59 ERR_FAIL_V_MSG(Ref<Texture2D>(), "EditorResourcePreviewGenerator::_generate needs to be overridden.");
60}
61
62Ref<Texture2D> EditorResourcePreviewGenerator::generate_from_path(const String &p_path, const Size2 &p_size, Dictionary &p_metadata) const {
63 Ref<Texture2D> preview;
64 if (GDVIRTUAL_CALL(_generate_from_path, p_path, p_size, p_metadata, preview)) {
65 return preview;
66 }
67
68 Ref<Resource> res = ResourceLoader::load(p_path);
69 if (!res.is_valid()) {
70 return res;
71 }
72 return generate(res, p_size, p_metadata);
73}
74
75bool EditorResourcePreviewGenerator::generate_small_preview_automatically() const {
76 bool success = false;
77 GDVIRTUAL_CALL(_generate_small_preview_automatically, success);
78 return success;
79}
80
81bool EditorResourcePreviewGenerator::can_generate_small_preview() const {
82 bool success = false;
83 GDVIRTUAL_CALL(_can_generate_small_preview, success);
84 return success;
85}
86
87void EditorResourcePreviewGenerator::_bind_methods() {
88 GDVIRTUAL_BIND(_handles, "type");
89 GDVIRTUAL_BIND(_generate, "resource", "size", "metadata");
90 GDVIRTUAL_BIND(_generate_from_path, "path", "size", "metadata");
91 GDVIRTUAL_BIND(_generate_small_preview_automatically);
92 GDVIRTUAL_BIND(_can_generate_small_preview);
93}
94
95EditorResourcePreviewGenerator::EditorResourcePreviewGenerator() {
96}
97
98EditorResourcePreview *EditorResourcePreview::singleton = nullptr;
99
100void EditorResourcePreview::_thread_func(void *ud) {
101 EditorResourcePreview *erp = (EditorResourcePreview *)ud;
102 erp->_thread();
103}
104
105void EditorResourcePreview::_preview_ready(const String &p_path, int p_hash, const Ref<Texture2D> &p_texture, const Ref<Texture2D> &p_small_texture, ObjectID id, const StringName &p_func, const Variant &p_ud, const Dictionary &p_metadata) {
106 {
107 MutexLock lock(preview_mutex);
108
109 uint64_t modified_time = 0;
110
111 if (!p_path.begins_with("ID:")) {
112 modified_time = FileAccess::get_modified_time(p_path);
113 }
114
115 Item item;
116 item.order = order++;
117 item.preview = p_texture;
118 item.small_preview = p_small_texture;
119 item.last_hash = p_hash;
120 item.modified_time = modified_time;
121 item.preview_metadata = p_metadata;
122
123 cache[p_path] = item;
124 }
125
126 MessageQueue::get_singleton()->push_call(id, p_func, p_path, p_texture, p_small_texture, p_ud);
127}
128
129void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base, Dictionary &p_metadata) {
130 String type;
131
132 if (p_item.resource.is_valid()) {
133 type = p_item.resource->get_class();
134 } else {
135 type = ResourceLoader::get_resource_type(p_item.path);
136 }
137
138 if (type.is_empty()) {
139 r_texture = Ref<ImageTexture>();
140 r_small_texture = Ref<ImageTexture>();
141 return; //could not guess type
142 }
143
144 int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");
145 thumbnail_size *= EDSCALE;
146
147 r_texture = Ref<ImageTexture>();
148 r_small_texture = Ref<ImageTexture>();
149
150 for (int i = 0; i < preview_generators.size(); i++) {
151 if (!preview_generators[i]->handles(type)) {
152 continue;
153 }
154
155 Ref<Texture2D> generated;
156 if (p_item.resource.is_valid()) {
157 generated = preview_generators.write[i]->generate(p_item.resource, Vector2(thumbnail_size, thumbnail_size), p_metadata);
158 } else {
159 generated = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(thumbnail_size, thumbnail_size), p_metadata);
160 }
161 r_texture = generated;
162
163 if (preview_generators[i]->can_generate_small_preview()) {
164 Ref<Texture2D> generated_small;
165 Dictionary d;
166 if (p_item.resource.is_valid()) {
167 generated_small = preview_generators.write[i]->generate(p_item.resource, Vector2(small_thumbnail_size, small_thumbnail_size), d);
168 } else {
169 generated_small = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(small_thumbnail_size, small_thumbnail_size), d);
170 }
171 r_small_texture = generated_small;
172 }
173
174 if (!r_small_texture.is_valid() && r_texture.is_valid() && preview_generators[i]->generate_small_preview_automatically()) {
175 Ref<Image> small_image = r_texture->get_image();
176 small_image = small_image->duplicate();
177 small_image->resize(small_thumbnail_size, small_thumbnail_size, Image::INTERPOLATE_CUBIC);
178 r_small_texture.instantiate();
179 r_small_texture->set_image(small_image);
180 }
181
182 break;
183 }
184
185 if (!p_item.resource.is_valid()) {
186 // Cache the preview in case it's a resource on disk.
187 if (r_texture.is_valid()) {
188 // Wow it generated a preview... save cache.
189 bool has_small_texture = r_small_texture.is_valid();
190 ResourceSaver::save(r_texture, cache_base + ".png");
191 if (has_small_texture) {
192 ResourceSaver::save(r_small_texture, cache_base + "_small.png");
193 }
194 Ref<FileAccess> f = FileAccess::open(cache_base + ".txt", FileAccess::WRITE);
195 ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + cache_base + ".txt'. Check user write permissions.");
196 _write_preview_cache(f, thumbnail_size, has_small_texture, FileAccess::get_modified_time(p_item.path), FileAccess::get_md5(p_item.path), p_metadata);
197 }
198 }
199}
200
201const Dictionary EditorResourcePreview::get_preview_metadata(const String &p_path) const {
202 ERR_FAIL_COND_V(!cache.has(p_path), Dictionary());
203 return cache[p_path].preview_metadata;
204}
205
206void EditorResourcePreview::_iterate() {
207 preview_mutex.lock();
208
209 if (queue.size()) {
210 QueueItem item = queue.front()->get();
211 queue.pop_front();
212
213 if (cache.has(item.path)) {
214 // Already has it because someone loaded it, just let it know it's ready.
215 _preview_ready(item.path, cache[item.path].last_hash, cache[item.path].preview, cache[item.path].small_preview, item.id, item.function, item.userdata, cache[item.path].preview_metadata);
216
217 preview_mutex.unlock();
218 } else {
219 preview_mutex.unlock();
220
221 Ref<ImageTexture> texture;
222 Ref<ImageTexture> small_texture;
223
224 int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");
225 thumbnail_size *= EDSCALE;
226
227 if (item.resource.is_valid()) {
228 Dictionary preview_metadata;
229 _generate_preview(texture, small_texture, item, String(), preview_metadata);
230
231 _preview_ready(item.path, item.resource->hash_edited_version(), texture, small_texture, item.id, item.function, item.userdata, preview_metadata);
232
233 } else {
234 Dictionary preview_metadata;
235 String temp_path = EditorPaths::get_singleton()->get_cache_dir();
236 String cache_base = ProjectSettings::get_singleton()->globalize_path(item.path).md5_text();
237 cache_base = temp_path.path_join("resthumb-" + cache_base);
238
239 // Does not have it, try to load a cached thumbnail.
240
241 String file = cache_base + ".txt";
242 Ref<FileAccess> f = FileAccess::open(file, FileAccess::READ);
243 if (f.is_null()) {
244 // No cache found, generate.
245 _generate_preview(texture, small_texture, item, cache_base, preview_metadata);
246 } else {
247 uint64_t modtime = FileAccess::get_modified_time(item.path);
248 int tsize;
249 bool has_small_texture;
250 uint64_t last_modtime;
251 String hash;
252 _read_preview_cache(f, &tsize, &has_small_texture, &last_modtime, &hash, &preview_metadata);
253
254 bool cache_valid = true;
255
256 if (tsize != thumbnail_size) {
257 cache_valid = false;
258 f.unref();
259 } else if (last_modtime != modtime) {
260 String last_md5 = f->get_line();
261 String md5 = FileAccess::get_md5(item.path);
262 f.unref();
263
264 if (last_md5 != md5) {
265 cache_valid = false;
266 } else {
267 // Update modified time.
268
269 Ref<FileAccess> f2 = FileAccess::open(file, FileAccess::WRITE);
270 if (f2.is_null()) {
271 // Not returning as this would leave the thread hanging and would require
272 // some proper cleanup/disabling of resource preview generation.
273 ERR_PRINT("Cannot create file '" + file + "'. Check user write permissions.");
274 } else {
275 _write_preview_cache(f2, thumbnail_size, has_small_texture, modtime, md5, preview_metadata);
276 }
277 }
278 } else {
279 f.unref();
280 }
281
282 if (cache_valid) {
283 Ref<Image> img;
284 img.instantiate();
285 Ref<Image> small_img;
286 small_img.instantiate();
287
288 if (img->load(cache_base + ".png") != OK) {
289 cache_valid = false;
290 } else {
291 texture.instantiate();
292 texture->set_image(img);
293
294 if (has_small_texture) {
295 if (small_img->load(cache_base + "_small.png") != OK) {
296 cache_valid = false;
297 } else {
298 small_texture.instantiate();
299 small_texture->set_image(small_img);
300 }
301 }
302 }
303 }
304
305 if (!cache_valid) {
306 _generate_preview(texture, small_texture, item, cache_base, preview_metadata);
307 }
308 }
309 _preview_ready(item.path, 0, texture, small_texture, item.id, item.function, item.userdata, preview_metadata);
310 }
311 }
312
313 } else {
314 preview_mutex.unlock();
315 }
316}
317
318void EditorResourcePreview::_write_preview_cache(Ref<FileAccess> p_file, int p_thumbnail_size, bool p_has_small_texture, uint64_t p_modified_time, String p_hash, const Dictionary &p_metadata) {
319 p_file->store_line(itos(p_thumbnail_size));
320 p_file->store_line(itos(p_has_small_texture));
321 p_file->store_line(itos(p_modified_time));
322 p_file->store_line(p_hash);
323 p_file->store_line(VariantUtilityFunctions::var_to_str(p_metadata).replace("\n", " "));
324}
325
326void EditorResourcePreview::_read_preview_cache(Ref<FileAccess> p_file, int *r_thumbnail_size, bool *r_has_small_texture, uint64_t *r_modified_time, String *r_hash, Dictionary *r_metadata) {
327 *r_thumbnail_size = p_file->get_line().to_int();
328 *r_has_small_texture = p_file->get_line().to_int();
329 *r_modified_time = p_file->get_line().to_int();
330 *r_hash = p_file->get_line();
331 *r_metadata = VariantUtilityFunctions::str_to_var(p_file->get_line());
332}
333
334void EditorResourcePreview::_thread() {
335 exited.clear();
336 while (!exit.is_set()) {
337 preview_sem.wait();
338 _iterate();
339 }
340 exited.set();
341}
342
343void EditorResourcePreview::_update_thumbnail_sizes() {
344 if (small_thumbnail_size == -1) {
345 // Kind of a workaround to retrieve the default icon size.
346 small_thumbnail_size = EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Object"), EditorStringName(EditorIcons))->get_width();
347 }
348}
349
350void EditorResourcePreview::queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {
351 ERR_FAIL_NULL(p_receiver);
352 ERR_FAIL_COND(!p_res.is_valid());
353 _update_thumbnail_sizes();
354
355 {
356 MutexLock lock(preview_mutex);
357
358 String path_id = "ID:" + itos(p_res->get_instance_id());
359
360 if (cache.has(path_id) && cache[path_id].last_hash == p_res->hash_edited_version()) {
361 cache[path_id].order = order++;
362 p_receiver->call(p_receiver_func, path_id, cache[path_id].preview, cache[path_id].small_preview, p_userdata);
363 return;
364 }
365
366 cache.erase(path_id); //erase if exists, since it will be regen
367
368 QueueItem item;
369 item.function = p_receiver_func;
370 item.id = p_receiver->get_instance_id();
371 item.resource = p_res;
372 item.path = path_id;
373 item.userdata = p_userdata;
374
375 queue.push_back(item);
376 }
377 preview_sem.post();
378}
379
380void EditorResourcePreview::queue_resource_preview(const String &p_path, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {
381 _update_thumbnail_sizes();
382
383 ERR_FAIL_NULL(p_receiver);
384 {
385 MutexLock lock(preview_mutex);
386
387 if (cache.has(p_path)) {
388 cache[p_path].order = order++;
389 p_receiver->call(p_receiver_func, p_path, cache[p_path].preview, cache[p_path].small_preview, p_userdata);
390 return;
391 }
392
393 QueueItem item;
394 item.function = p_receiver_func;
395 item.id = p_receiver->get_instance_id();
396 item.path = p_path;
397 item.userdata = p_userdata;
398
399 queue.push_back(item);
400 }
401 preview_sem.post();
402}
403
404void EditorResourcePreview::add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {
405 preview_generators.push_back(p_generator);
406}
407
408void EditorResourcePreview::remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {
409 preview_generators.erase(p_generator);
410}
411
412EditorResourcePreview *EditorResourcePreview::get_singleton() {
413 return singleton;
414}
415
416void EditorResourcePreview::_bind_methods() {
417 ClassDB::bind_method("_preview_ready", &EditorResourcePreview::_preview_ready);
418
419 ClassDB::bind_method(D_METHOD("queue_resource_preview", "path", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::queue_resource_preview);
420 ClassDB::bind_method(D_METHOD("queue_edited_resource_preview", "resource", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::queue_edited_resource_preview);
421 ClassDB::bind_method(D_METHOD("add_preview_generator", "generator"), &EditorResourcePreview::add_preview_generator);
422 ClassDB::bind_method(D_METHOD("remove_preview_generator", "generator"), &EditorResourcePreview::remove_preview_generator);
423 ClassDB::bind_method(D_METHOD("check_for_invalidation", "path"), &EditorResourcePreview::check_for_invalidation);
424
425 ADD_SIGNAL(MethodInfo("preview_invalidated", PropertyInfo(Variant::STRING, "path")));
426}
427
428void EditorResourcePreview::check_for_invalidation(const String &p_path) {
429 bool call_invalidated = false;
430 {
431 MutexLock lock(preview_mutex);
432
433 if (cache.has(p_path)) {
434 uint64_t modified_time = FileAccess::get_modified_time(p_path);
435 if (modified_time != cache[p_path].modified_time) {
436 cache.erase(p_path);
437 call_invalidated = true;
438 }
439 }
440 }
441
442 if (call_invalidated) { //do outside mutex
443 call_deferred(SNAME("emit_signal"), "preview_invalidated", p_path);
444 }
445}
446
447void EditorResourcePreview::start() {
448 if (DisplayServer::get_singleton()->get_name() != "headless") {
449 ERR_FAIL_COND_MSG(thread.is_started(), "Thread already started.");
450 thread.start(_thread_func, this);
451 }
452}
453
454void EditorResourcePreview::stop() {
455 if (thread.is_started()) {
456 exit.set();
457 preview_sem.post();
458 while (!exited.is_set()) {
459 OS::get_singleton()->delay_usec(10000);
460 RenderingServer::get_singleton()->sync(); //sync pending stuff, as thread may be blocked on rendering server
461 }
462 thread.wait_to_finish();
463 }
464}
465
466EditorResourcePreview::EditorResourcePreview() {
467 singleton = this;
468 order = 0;
469}
470
471EditorResourcePreview::~EditorResourcePreview() {
472 stop();
473}
474