Path: blob/master/editor/inspector/editor_resource_preview.cpp
20901 views
/**************************************************************************/1/* editor_resource_preview.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "editor_resource_preview.h"3132#include "core/config/project_settings.h"33#include "core/io/file_access.h"34#include "core/io/resource_loader.h"35#include "core/io/resource_saver.h"36#include "core/variant/variant_utility.h"37#include "editor/editor_node.h"38#include "editor/editor_string_names.h"39#include "editor/file_system/editor_paths.h"40#include "editor/settings/editor_settings.h"41#include "editor/themes/editor_scale.h"42#include "scene/main/window.h"43#include "scene/resources/image_texture.h"44#include "servers/rendering/rendering_server_globals.h"4546bool EditorResourcePreviewGenerator::handles(const String &p_type) const {47bool success = false;48GDVIRTUAL_CALL(_handles, p_type, success);49return success;50}5152Ref<Texture2D> EditorResourcePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {53Ref<Texture2D> preview;54GDVIRTUAL_CALL(_generate, p_from, p_size, p_metadata, preview);55return preview;56}5758Ref<Texture2D> EditorResourcePreviewGenerator::generate_from_path(const String &p_path, const Size2 &p_size, Dictionary &p_metadata) const {59Ref<Texture2D> preview;60if (GDVIRTUAL_CALL(_generate_from_path, p_path, p_size, p_metadata, preview)) {61return preview;62}6364Ref<Resource> res = ResourceLoader::load(p_path);65if (res.is_null()) {66return res;67}68return generate(res, p_size, p_metadata);69}7071bool EditorResourcePreviewGenerator::generate_small_preview_automatically() const {72bool success = false;73GDVIRTUAL_CALL(_generate_small_preview_automatically, success);74return success;75}7677bool EditorResourcePreviewGenerator::can_generate_small_preview() const {78bool success = false;79GDVIRTUAL_CALL(_can_generate_small_preview, success);80return success;81}8283void EditorResourcePreviewGenerator::_bind_methods() {84GDVIRTUAL_BIND(_handles, "type");85GDVIRTUAL_BIND(_generate, "resource", "size", "metadata");86GDVIRTUAL_BIND(_generate_from_path, "path", "size", "metadata");87GDVIRTUAL_BIND(_generate_small_preview_automatically);88GDVIRTUAL_BIND(_can_generate_small_preview);8990ClassDB::bind_method(D_METHOD("request_draw_and_wait", "viewport"), &EditorResourcePreviewGenerator::request_draw_and_wait);91}9293void EditorResourcePreviewGenerator::DrawRequester::request_and_wait(RID p_viewport) {94if (EditorResourcePreview::get_singleton()->is_threaded()) {95RS::get_singleton()->connect(SNAME("frame_pre_draw"), callable_mp(this, &EditorResourcePreviewGenerator::DrawRequester::_prepare_draw).bind(p_viewport), Object::CONNECT_ONE_SHOT);96semaphore.wait();97} else {98// Avoid the main viewport and children being redrawn.99SceneTree *st = Object::cast_to<SceneTree>(OS::get_singleton()->get_main_loop());100ERR_FAIL_NULL_MSG(st, "Editor's MainLoop is not a SceneTree. This is a bug.");101RID root_vp = st->get_root()->get_viewport_rid();102RenderingServer::get_singleton()->viewport_set_active(root_vp, false);103104RS::get_singleton()->viewport_set_update_mode(p_viewport, RS::VIEWPORT_UPDATE_ONCE);105RS::get_singleton()->draw(false);106107// Let main viewport and children be drawn again.108RenderingServer::get_singleton()->viewport_set_active(root_vp, true);109}110}111112void EditorResourcePreviewGenerator::DrawRequester::abort() {113if (EditorResourcePreview::get_singleton()->is_threaded()) {114semaphore.post();115}116}117118void EditorResourcePreviewGenerator::request_draw_and_wait(RID viewport) const {119DrawRequester draw_requester;120draw_requester.request_and_wait(viewport);121}122123void EditorResourcePreviewGenerator::DrawRequester::_prepare_draw(RID p_viewport) {124RS::get_singleton()->viewport_set_update_mode(p_viewport, RS::VIEWPORT_UPDATE_ONCE);125RS::get_singleton()->request_frame_drawn_callback(callable_mp(this, &EditorResourcePreviewGenerator::DrawRequester::_post_semaphore));126}127128void EditorResourcePreviewGenerator::DrawRequester::_post_semaphore() {129semaphore.post();130}131132bool EditorResourcePreview::is_threaded() const {133return RSG::rasterizer->can_create_resources_async();134}135136void EditorResourcePreview::_thread_func(void *ud) {137EditorResourcePreview *erp = (EditorResourcePreview *)ud;138erp->_thread();139}140141void EditorResourcePreview::_preview_ready(const String &p_path, int p_hash, const Ref<Texture2D> &p_texture, const Ref<Texture2D> &p_small_texture, const Callable &p_callback, const Dictionary &p_metadata) {142{143MutexLock lock(preview_mutex);144145uint64_t modified_time = 0;146147if (!p_path.begins_with("ID:")) {148modified_time = FileAccess::get_modified_time(p_path);149String import_path = p_path + ".import";150if (FileAccess::exists(import_path)) {151modified_time = MAX(modified_time, FileAccess::get_modified_time(import_path));152}153}154155Item item;156item.preview = p_texture;157item.small_preview = p_small_texture;158item.last_hash = p_hash;159item.modified_time = modified_time;160item.preview_metadata = p_metadata;161162cache[p_path] = item;163}164p_callback.call_deferred(p_path, p_texture, p_small_texture);165}166167void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base, Dictionary &p_metadata) {168String type;169170uint64_t started_at = OS::get_singleton()->get_ticks_usec();171172if (p_item.resource.is_valid()) {173type = p_item.resource->get_class();174} else {175type = ResourceLoader::get_resource_type(p_item.path);176}177178if (type.is_empty()) {179r_texture = Ref<ImageTexture>();180r_small_texture = Ref<ImageTexture>();181182if (is_print_verbose_enabled()) {183print_line(vformat("Generated '%s' preview in %d usec", p_item.path, OS::get_singleton()->get_ticks_usec() - started_at));184}185return; //could not guess type186}187188int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");189thumbnail_size *= EDSCALE;190191r_texture = Ref<ImageTexture>();192r_small_texture = Ref<ImageTexture>();193194for (int i = 0; i < preview_generators.size(); i++) {195if (!preview_generators[i]->handles(type)) {196continue;197}198199Ref<Texture2D> generated;200if (p_item.resource.is_valid()) {201generated = preview_generators.write[i]->generate(p_item.resource, Vector2(thumbnail_size, thumbnail_size), p_metadata);202} else {203generated = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(thumbnail_size, thumbnail_size), p_metadata);204}205r_texture = generated;206207if (preview_generators[i]->can_generate_small_preview()) {208Ref<Texture2D> generated_small;209Dictionary d;210if (p_item.resource.is_valid()) {211generated_small = preview_generators.write[i]->generate(p_item.resource, Vector2(small_thumbnail_size, small_thumbnail_size), d);212} else {213generated_small = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(small_thumbnail_size, small_thumbnail_size), d);214}215r_small_texture = generated_small;216}217218if (r_small_texture.is_null() && r_texture.is_valid() && preview_generators[i]->generate_small_preview_automatically()) {219Ref<Image> small_image = r_texture->get_image()->duplicate();220Vector2i new_size = Vector2i(1, 1) * small_thumbnail_size;221const real_t aspect = small_image->get_size().aspect();222if (aspect > 1.0) {223new_size.y = MAX(1, new_size.y / aspect);224} else if (aspect < 1.0) {225new_size.x = MAX(1, new_size.x * aspect);226}227small_image->resize(new_size.x, new_size.y, Image::INTERPOLATE_CUBIC);228229// Make sure the image is always square.230if (aspect != 1.0) {231Ref<Image> rect = small_image;232const Vector2i rect_size = rect->get_size();233small_image = Image::create_empty(small_thumbnail_size, small_thumbnail_size, false, rect->get_format());234// Blit the rectangle in the center of the square.235small_image->blit_rect(rect, Rect2i(Vector2i(), rect_size), (Vector2i(1, 1) * small_thumbnail_size - rect_size) / 2);236}237238r_small_texture.instantiate();239r_small_texture->set_image(small_image);240}241242if (generated.is_valid()) {243break;244}245}246247if (p_item.resource.is_null()) {248// Cache the preview in case it's a resource on disk.249if (r_texture.is_valid()) {250// Wow it generated a preview... save cache.251bool has_small_texture = r_small_texture.is_valid();252ResourceSaver::save(r_texture, cache_base + ".png");253if (has_small_texture) {254ResourceSaver::save(r_small_texture, cache_base + "_small.png");255}256Ref<FileAccess> f = FileAccess::open(cache_base + ".txt", FileAccess::WRITE);257ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + cache_base + ".txt'. Check user write permissions.");258259uint64_t modtime = FileAccess::get_modified_time(p_item.path);260String import_path = p_item.path + ".import";261if (FileAccess::exists(import_path)) {262modtime = MAX(modtime, FileAccess::get_modified_time(import_path));263}264265_write_preview_cache(f, thumbnail_size, has_small_texture, modtime, FileAccess::get_md5(p_item.path), p_metadata);266}267}268269if (is_print_verbose_enabled()) {270print_line(vformat("Generated '%s' preview in %d usec", p_item.path, OS::get_singleton()->get_ticks_usec() - started_at));271}272}273274const Dictionary EditorResourcePreview::get_preview_metadata(const String &p_path) const {275ERR_FAIL_COND_V(!cache.has(p_path), Dictionary());276return cache[p_path].preview_metadata;277}278279void EditorResourcePreview::_iterate() {280preview_mutex.lock();281282if (queue.is_empty()) {283preview_mutex.unlock();284return;285}286287QueueItem item = queue.front()->get();288queue.pop_front();289290if (cache.has(item.path)) {291Item cached_item = cache[item.path];292// Already has it because someone loaded it, just let it know it's ready.293_preview_ready(item.path, cached_item.last_hash, cached_item.preview, cached_item.small_preview, item.callback, cached_item.preview_metadata);294preview_mutex.unlock();295return;296}297preview_mutex.unlock();298299Ref<ImageTexture> texture;300Ref<ImageTexture> small_texture;301302int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");303thumbnail_size *= EDSCALE;304305if (item.resource.is_valid()) {306Dictionary preview_metadata;307_generate_preview(texture, small_texture, item, String(), preview_metadata);308_preview_ready(item.path, item.resource->hash_edited_version_for_preview(), texture, small_texture, item.callback, preview_metadata);309return;310}311312Dictionary preview_metadata;313String temp_path = EditorPaths::get_singleton()->get_cache_dir();314String cache_base = ProjectSettings::get_singleton()->globalize_path(item.path).md5_text();315cache_base = temp_path.path_join("resthumb-" + cache_base);316317// Does not have it, try to load a cached thumbnail.318319String file = cache_base + ".txt";320Ref<FileAccess> f = FileAccess::open(file, FileAccess::READ);321if (f.is_null()) {322// No cache found, generate.323_generate_preview(texture, small_texture, item, cache_base, preview_metadata);324} else {325uint64_t modtime = FileAccess::get_modified_time(item.path);326String import_path = item.path + ".import";327if (FileAccess::exists(import_path)) {328modtime = MAX(modtime, FileAccess::get_modified_time(import_path));329}330331int tsize;332bool has_small_texture;333uint64_t last_modtime;334String hash;335bool outdated;336_read_preview_cache(f, &tsize, &has_small_texture, &last_modtime, &hash, &preview_metadata, &outdated);337338bool cache_valid = true;339340if (tsize != thumbnail_size) {341cache_valid = false;342f.unref();343} else if (outdated) {344cache_valid = false;345f.unref();346} else if (last_modtime != modtime) {347String last_md5 = f->get_line();348String md5 = FileAccess::get_md5(item.path);349f.unref();350351if (last_md5 != md5) {352cache_valid = false;353} else {354// Update modified time.355356Ref<FileAccess> f2 = FileAccess::open(file, FileAccess::WRITE);357if (f2.is_null()) {358// Not returning as this would leave the thread hanging and would require359// some proper cleanup/disabling of resource preview generation.360ERR_PRINT("Cannot create file '" + file + "'. Check user write permissions.");361} else {362_write_preview_cache(f2, thumbnail_size, has_small_texture, modtime, md5, preview_metadata);363}364}365} else {366f.unref();367}368369if (cache_valid) {370Ref<Image> img;371img.instantiate();372Ref<Image> small_img;373small_img.instantiate();374375if (img->load(cache_base + ".png") != OK) {376cache_valid = false;377} else {378texture.instantiate();379texture->set_image(img);380381if (has_small_texture) {382if (small_img->load(cache_base + "_small.png") != OK) {383cache_valid = false;384} else {385small_texture.instantiate();386small_texture->set_image(small_img);387}388}389}390}391392if (!cache_valid) {393_generate_preview(texture, small_texture, item, cache_base, preview_metadata);394}395}396_preview_ready(item.path, 0, texture, small_texture, item.callback, preview_metadata);397}398399void EditorResourcePreview::_write_preview_cache(Ref<FileAccess> p_file, int p_thumbnail_size, bool p_has_small_texture, uint64_t p_modified_time, const String &p_hash, const Dictionary &p_metadata) {400p_file->store_line(itos(p_thumbnail_size));401p_file->store_line(itos(p_has_small_texture));402p_file->store_line(itos(p_modified_time));403p_file->store_line(p_hash);404p_file->store_line(VariantUtilityFunctions::var_to_str(p_metadata).replace_char('\n', ' '));405p_file->store_line(itos(CURRENT_METADATA_VERSION));406}407408void 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, bool *r_outdated) {409*r_thumbnail_size = p_file->get_line().to_int();410*r_has_small_texture = p_file->get_line().to_int();411*r_modified_time = p_file->get_line().to_int();412*r_hash = p_file->get_line();413*r_metadata = VariantUtilityFunctions::str_to_var(p_file->get_line());414*r_outdated = p_file->get_line().to_int() < CURRENT_METADATA_VERSION;415}416417void EditorResourcePreview::_thread() {418exited.clear();419while (!exiting.is_set()) {420preview_sem.wait();421_iterate();422}423exited.set();424}425426void EditorResourcePreview::_idle_callback() {427if (!singleton) {428// Just in case the shutdown of the editor involves the deletion of the singleton429// happening while additional idle callbacks can happen.430return;431}432433// Process preview tasks, trying to leave a little bit of responsiveness worst case.434uint64_t start = OS::get_singleton()->get_ticks_msec();435while (!singleton->queue.is_empty() && OS::get_singleton()->get_ticks_msec() - start < 100) {436singleton->_iterate();437}438}439440void EditorResourcePreview::_update_thumbnail_sizes() {441if (small_thumbnail_size == -1) {442// Kind of a workaround to retrieve the default icon size.443small_thumbnail_size = EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Object"), EditorStringName(EditorIcons))->get_width();444}445}446447EditorResourcePreview::PreviewItem EditorResourcePreview::get_resource_preview_if_available(const String &p_path) {448PreviewItem item;449{450MutexLock lock(preview_mutex);451452HashMap<String, EditorResourcePreview::Item>::Iterator I = cache.find(p_path);453if (!I) {454return item;455}456457EditorResourcePreview::Item &cached_item = I->value;458item.preview = cached_item.preview;459item.small_preview = cached_item.small_preview;460}461preview_sem.post();462return item;463}464465void EditorResourcePreview::_queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {466ERR_FAIL_NULL(p_receiver);467queue_edited_resource_preview(p_res, Callable(p_receiver, p_receiver_func).bind(p_userdata));468}469470void EditorResourcePreview::queue_edited_resource_preview(const Ref<Resource> &p_res, const Callable &p_callback) {471ERR_FAIL_COND(p_res.is_null());472_update_thumbnail_sizes();473474{475MutexLock lock(preview_mutex);476477String path_id = "ID:" + itos(p_res->get_instance_id());478HashMap<String, EditorResourcePreview::Item>::Iterator I = cache.find(path_id);479480if (I && I->value.last_hash == p_res->hash_edited_version_for_preview()) {481p_callback.call(path_id, I->value.preview, I->value.small_preview);482return;483}484485if (I) {486cache.remove(I); // Erase if exists, since it will be regen.487}488489QueueItem item;490item.resource = p_res;491item.path = path_id;492item.callback = p_callback;493queue.push_back(item);494}495preview_sem.post();496}497498void EditorResourcePreview::_queue_resource_preview(const String &p_path, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {499ERR_FAIL_NULL(p_receiver);500queue_resource_preview(p_path, Callable(p_receiver, p_receiver_func).bind(p_userdata));501}502503void EditorResourcePreview::queue_resource_preview(const String &p_path, const Callable &p_callback) {504_update_thumbnail_sizes();505506{507MutexLock lock(preview_mutex);508509const Item *cached_item = cache.getptr(p_path);510if (cached_item) {511p_callback.call(p_path, cached_item->preview, cached_item->small_preview);512return;513}514515QueueItem item;516item.path = p_path;517item.callback = p_callback;518queue.push_back(item);519}520preview_sem.post();521}522523void EditorResourcePreview::add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {524preview_generators.push_back(p_generator);525}526527void EditorResourcePreview::remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {528preview_generators.erase(p_generator);529}530531EditorResourcePreview *EditorResourcePreview::get_singleton() {532return singleton;533}534535void EditorResourcePreview::_bind_methods() {536ClassDB::bind_method(D_METHOD("queue_resource_preview", "path", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::_queue_resource_preview);537ClassDB::bind_method(D_METHOD("queue_edited_resource_preview", "resource", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::_queue_edited_resource_preview);538ClassDB::bind_method(D_METHOD("add_preview_generator", "generator"), &EditorResourcePreview::add_preview_generator);539ClassDB::bind_method(D_METHOD("remove_preview_generator", "generator"), &EditorResourcePreview::remove_preview_generator);540ClassDB::bind_method(D_METHOD("check_for_invalidation", "path"), &EditorResourcePreview::check_for_invalidation);541542ADD_SIGNAL(MethodInfo("preview_invalidated", PropertyInfo(Variant::STRING, "path")));543}544545void EditorResourcePreview::_notification(int p_what) {546switch (p_what) {547case NOTIFICATION_EXIT_TREE: {548stop();549} break;550}551}552553void EditorResourcePreview::check_for_invalidation(const String &p_path) {554bool call_invalidated = false;555{556MutexLock lock(preview_mutex);557558if (cache.has(p_path)) {559uint64_t modified_time = FileAccess::get_modified_time(p_path);560String import_path = p_path + ".import";561if (FileAccess::exists(import_path)) {562modified_time = MAX(modified_time, FileAccess::get_modified_time(import_path));563}564565if (modified_time != cache[p_path].modified_time) {566cache.erase(p_path);567call_invalidated = true;568}569}570}571572if (call_invalidated) { //do outside mutex573call_deferred(SNAME("emit_signal"), "preview_invalidated", p_path);574}575}576577void EditorResourcePreview::start() {578if (DisplayServer::get_singleton()->get_name() == "headless") {579return;580}581582if (is_threaded()) {583ERR_FAIL_COND_MSG(thread.is_started(), "Thread already started.");584thread.start(_thread_func, this);585} else {586SceneTree *st = Object::cast_to<SceneTree>(OS::get_singleton()->get_main_loop());587ERR_FAIL_NULL_MSG(st, "Editor's MainLoop is not a SceneTree. This is a bug.");588st->add_idle_callback(&_idle_callback);589}590}591592void EditorResourcePreview::stop() {593if (is_threaded()) {594if (thread.is_started()) {595exiting.set();596preview_sem.post();597598for (int i = 0; i < preview_generators.size(); i++) {599preview_generators.write[i]->abort();600}601602while (!exited.is_set()) {603// Sync pending work.604OS::get_singleton()->delay_usec(10000);605RenderingServer::get_singleton()->sync();606MessageQueue::get_singleton()->flush();607}608609thread.wait_to_finish();610}611}612}613614EditorResourcePreview::EditorResourcePreview() {615singleton = this;616}617618EditorResourcePreview::~EditorResourcePreview() {619stop();620}621622623