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 | |
46 | bool 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 | |
54 | Ref<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 | |
62 | Ref<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 | |
75 | bool EditorResourcePreviewGenerator::generate_small_preview_automatically() const { |
76 | bool success = false; |
77 | GDVIRTUAL_CALL(_generate_small_preview_automatically, success); |
78 | return success; |
79 | } |
80 | |
81 | bool EditorResourcePreviewGenerator::can_generate_small_preview() const { |
82 | bool success = false; |
83 | GDVIRTUAL_CALL(_can_generate_small_preview, success); |
84 | return success; |
85 | } |
86 | |
87 | void 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 | |
95 | EditorResourcePreviewGenerator::EditorResourcePreviewGenerator() { |
96 | } |
97 | |
98 | EditorResourcePreview *EditorResourcePreview::singleton = nullptr; |
99 | |
100 | void EditorResourcePreview::_thread_func(void *ud) { |
101 | EditorResourcePreview *erp = (EditorResourcePreview *)ud; |
102 | erp->_thread(); |
103 | } |
104 | |
105 | void 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 | |
129 | void 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 | |
201 | const 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 | |
206 | void 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 | |
318 | void 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 | |
326 | void 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 | |
334 | void EditorResourcePreview::_thread() { |
335 | exited.clear(); |
336 | while (!exit.is_set()) { |
337 | preview_sem.wait(); |
338 | _iterate(); |
339 | } |
340 | exited.set(); |
341 | } |
342 | |
343 | void 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 | |
350 | void 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 | |
380 | void 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 | |
404 | void EditorResourcePreview::add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) { |
405 | preview_generators.push_back(p_generator); |
406 | } |
407 | |
408 | void EditorResourcePreview::remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) { |
409 | preview_generators.erase(p_generator); |
410 | } |
411 | |
412 | EditorResourcePreview *EditorResourcePreview::get_singleton() { |
413 | return singleton; |
414 | } |
415 | |
416 | void 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 | |
428 | void 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 | |
447 | void 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 | |
454 | void 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 | |
466 | EditorResourcePreview::EditorResourcePreview() { |
467 | singleton = this; |
468 | order = 0; |
469 | } |
470 | |
471 | EditorResourcePreview::~EditorResourcePreview() { |
472 | stop(); |
473 | } |
474 | |