Skip to content

Commit

Permalink
Add metadata to resource previews
Browse files Browse the repository at this point in the history
  • Loading branch information
KoBeWi committed May 10, 2023
1 parent 74c34ae commit 04317e9
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 87 deletions.
4 changes: 4 additions & 0 deletions doc/classes/EditorResourcePreviewGenerator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,24 @@
<return type="Texture2D" />
<param index="0" name="resource" type="Resource" />
<param index="1" name="size" type="Vector2i" />
<param index="2" name="metadata" type="Dictionary" />
<description>
Generate a preview from a given resource with the specified size. This must always be implemented.
Returning an empty texture is an OK way to fail and let another generator take care.
Care must be taken because this function is always called from a thread (not the main thread).
[param metadata] dictionary can modified to store file-specific metadata that can be used by the editor (like image size, sample length etc.).
</description>
</method>
<method name="_generate_from_path" qualifiers="virtual const">
<return type="Texture2D" />
<param index="0" name="path" type="String" />
<param index="1" name="size" type="Vector2i" />
<param index="2" name="metadata" type="Dictionary" />
<description>
Generate a preview directly from a path with the specified size. Implementing this is optional, as default code will load and call [method _generate].
Returning an empty texture is an OK way to fail and let another generator take care.
Care must be taken because this function is always called from a thread (not the main thread).
[param metadata] dictionary can modified to store file-specific metadata that can be used by the editor (like image size, sample length etc.).
</description>
</method>
<method name="_generate_small_preview_automatically" qualifiers="virtual const">
Expand Down
115 changes: 63 additions & 52 deletions editor/editor_resource_preview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/object/message_queue.h"
#include "core/variant/variant_utility.cpp"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/editor_scale.h"
Expand All @@ -48,25 +49,25 @@ bool EditorResourcePreviewGenerator::handles(const String &p_type) const {
ERR_FAIL_V_MSG(false, "EditorResourcePreviewGenerator::_handles needs to be overridden.");
}

Ref<Texture2D> EditorResourcePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size) const {
Ref<Texture2D> EditorResourcePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {
Ref<Texture2D> preview;
if (GDVIRTUAL_CALL(_generate, p_from, p_size, preview)) {
if (GDVIRTUAL_CALL(_generate, p_from, p_size, p_metadata, preview)) {
return preview;
}
ERR_FAIL_V_MSG(Ref<Texture2D>(), "EditorResourcePreviewGenerator::_generate needs to be overridden.");
}

Ref<Texture2D> EditorResourcePreviewGenerator::generate_from_path(const String &p_path, const Size2 &p_size) const {
Ref<Texture2D> EditorResourcePreviewGenerator::generate_from_path(const String &p_path, const Size2 &p_size, Dictionary &p_metadata) const {
Ref<Texture2D> preview;
if (GDVIRTUAL_CALL(_generate_from_path, p_path, p_size, preview)) {
if (GDVIRTUAL_CALL(_generate_from_path, p_path, p_size, p_metadata, preview)) {
return preview;
}

Ref<Resource> res = ResourceLoader::load(p_path);
if (!res.is_valid()) {
return res;
}
return generate(res, p_size);
return generate(res, p_size, p_metadata);
}

bool EditorResourcePreviewGenerator::generate_small_preview_automatically() const {
Expand All @@ -83,8 +84,8 @@ bool EditorResourcePreviewGenerator::can_generate_small_preview() const {

void EditorResourcePreviewGenerator::_bind_methods() {
GDVIRTUAL_BIND(_handles, "type");
GDVIRTUAL_BIND(_generate, "resource", "size");
GDVIRTUAL_BIND(_generate_from_path, "path", "size");
GDVIRTUAL_BIND(_generate, "resource", "size", "metadata");
GDVIRTUAL_BIND(_generate_from_path, "path", "size", "metadata");
GDVIRTUAL_BIND(_generate_small_preview_automatically);
GDVIRTUAL_BIND(_can_generate_small_preview);
}
Expand All @@ -99,35 +100,31 @@ void EditorResourcePreview::_thread_func(void *ud) {
erp->_thread();
}

void EditorResourcePreview::_preview_ready(const String &p_str, const Ref<Texture2D> &p_texture, const Ref<Texture2D> &p_small_texture, ObjectID id, const StringName &p_func, const Variant &p_ud) {
String path = p_str;
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) {
{
MutexLock lock(preview_mutex);

uint32_t hash = 0;
uint64_t modified_time = 0;

if (p_str.begins_with("ID:")) {
hash = uint32_t(p_str.get_slicec(':', 2).to_int());
path = "ID:" + p_str.get_slicec(':', 1);
} else {
modified_time = FileAccess::get_modified_time(path);
if (!p_path.begins_with("ID:")) {
modified_time = FileAccess::get_modified_time(p_path);
}

Item item;
item.order = order++;
item.preview = p_texture;
item.small_preview = p_small_texture;
item.last_hash = hash;
item.last_hash = p_hash;
item.modified_time = modified_time;
item.preview_metadata = p_metadata;

cache[path] = item;
cache[p_path] = item;
}

MessageQueue::get_singleton()->push_call(id, p_func, path, p_texture, p_small_texture, p_ud);
MessageQueue::get_singleton()->push_call(id, p_func, p_path, p_texture, p_small_texture, p_ud);
}

void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base) {
void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base, Dictionary &p_metadata) {
String type;

if (p_item.resource.is_valid()) {
Expand Down Expand Up @@ -155,20 +152,21 @@ void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<

Ref<Texture2D> generated;
if (p_item.resource.is_valid()) {
generated = preview_generators[i]->generate(p_item.resource, Vector2(thumbnail_size, thumbnail_size));
generated = preview_generators.write[i]->generate(p_item.resource, Vector2(thumbnail_size, thumbnail_size), p_metadata);
} else {
generated = preview_generators[i]->generate_from_path(p_item.path, Vector2(thumbnail_size, thumbnail_size));
generated = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(thumbnail_size, thumbnail_size), p_metadata);
}
r_texture = generated;

int small_thumbnail_size = EditorNode::get_singleton()->get_theme_base()->get_theme_icon(SNAME("Object"), SNAME("EditorIcons"))->get_width(); // Kind of a workaround to retrieve the default icon size

if (preview_generators[i]->can_generate_small_preview()) {
Ref<Texture2D> generated_small;
Dictionary d;
if (p_item.resource.is_valid()) {
generated_small = preview_generators[i]->generate(p_item.resource, Vector2(small_thumbnail_size, small_thumbnail_size));
generated_small = preview_generators.write[i]->generate(p_item.resource, Vector2(small_thumbnail_size, small_thumbnail_size), d);
} else {
generated_small = preview_generators[i]->generate_from_path(p_item.path, Vector2(small_thumbnail_size, small_thumbnail_size));
generated_small = preview_generators.write[i]->generate_from_path(p_item.path, Vector2(small_thumbnail_size, small_thumbnail_size), d);
}
r_small_texture = generated_small;
}
Expand All @@ -185,24 +183,26 @@ void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<
}

if (!p_item.resource.is_valid()) {
// cache the preview in case it's a resource on disk
// Cache the preview in case it's a resource on disk.
if (r_texture.is_valid()) {
//wow it generated a preview... save cache
// Wow it generated a preview... save cache.
bool has_small_texture = r_small_texture.is_valid();
ResourceSaver::save(r_texture, cache_base + ".png");
if (has_small_texture) {
ResourceSaver::save(r_small_texture, cache_base + "_small.png");
}
Ref<FileAccess> f = FileAccess::open(cache_base + ".txt", FileAccess::WRITE);
ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + cache_base + ".txt'. Check user write permissions.");
f->store_line(itos(thumbnail_size));
f->store_line(itos(has_small_texture));
f->store_line(itos(FileAccess::get_modified_time(p_item.path)));
f->store_line(FileAccess::get_md5(p_item.path));
_write_preview_cache(f, thumbnail_size, has_small_texture, FileAccess::get_modified_time(p_item.path), FileAccess::get_md5(p_item.path), p_metadata);
}
}
}

Variant EditorResourcePreview::get_preview_metadata(const String &p_path, const String &p_meta) const {
ERR_FAIL_COND_V(!cache.has(p_path), Variant());
return cache[p_path].preview_metadata.get(p_meta, Variant());
}

void EditorResourcePreview::_iterate() {
preview_mutex.lock();

Expand All @@ -211,13 +211,8 @@ void EditorResourcePreview::_iterate() {
queue.pop_front();

if (cache.has(item.path)) {
//already has it because someone loaded it, just let it know it's ready
String path = item.path;
if (item.resource.is_valid()) {
path += ":" + itos(cache[item.path].last_hash); //keep last hash (see description of what this is in condition below)
}

_preview_ready(path, cache[item.path].preview, cache[item.path].small_preview, item.id, item.function, item.userdata);
// Already has it because someone loaded it, just let it know it's ready.
_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);

preview_mutex.unlock();
} else {
Expand All @@ -230,28 +225,31 @@ void EditorResourcePreview::_iterate() {
thumbnail_size *= EDSCALE;

if (item.resource.is_valid()) {
_generate_preview(texture, small_texture, item, String());
Dictionary preview_metadata;
_generate_preview(texture, small_texture, item, String(), preview_metadata);

//adding hash to the end of path (should be ID:<objid>:<hash>) because of 5 argument limit to call_deferred
_preview_ready(item.path + ":" + itos(item.resource->hash_edited_version()), texture, small_texture, item.id, item.function, item.userdata);
_preview_ready(item.path, item.resource->hash_edited_version(), texture, small_texture, item.id, item.function, item.userdata, preview_metadata);

} else {
Dictionary preview_metadata;
String temp_path = EditorPaths::get_singleton()->get_cache_dir();
String cache_base = ProjectSettings::get_singleton()->globalize_path(item.path).md5_text();
cache_base = temp_path.path_join("resthumb-" + cache_base);

//does not have it, try to load a cached thumbnail
// Does not have it, try to load a cached thumbnail.

String file = cache_base + ".txt";
Ref<FileAccess> f = FileAccess::open(file, FileAccess::READ);
if (f.is_null()) {
// No cache found, generate
_generate_preview(texture, small_texture, item, cache_base);
// No cache found, generate.
_generate_preview(texture, small_texture, item, cache_base, preview_metadata);
} else {
uint64_t modtime = FileAccess::get_modified_time(item.path);
int tsize = f->get_line().to_int();
bool has_small_texture = f->get_line().to_int();
uint64_t last_modtime = f->get_line().to_int();
int tsize;
bool has_small_texture;
uint64_t last_modtime;
String hash;
_read_preview_cache(f, &tsize, &has_small_texture, &last_modtime, &hash, &preview_metadata);

bool cache_valid = true;

Expand All @@ -266,18 +264,15 @@ void EditorResourcePreview::_iterate() {
if (last_md5 != md5) {
cache_valid = false;
} else {
//update modified time
// Update modified time.

Ref<FileAccess> f2 = FileAccess::open(file, FileAccess::WRITE);
if (f2.is_null()) {
// Not returning as this would leave the thread hanging and would require
// some proper cleanup/disabling of resource preview generation.
ERR_PRINT("Cannot create file '" + file + "'. Check user write permissions.");
} else {
f2->store_line(itos(thumbnail_size));
f2->store_line(itos(has_small_texture));
f2->store_line(itos(modtime));
f2->store_line(md5);
_write_preview_cache(f2, thumbnail_size, has_small_texture, modtime, md5, preview_metadata);
}
}
} else {
Expand Down Expand Up @@ -308,10 +303,10 @@ void EditorResourcePreview::_iterate() {
}

if (!cache_valid) {
_generate_preview(texture, small_texture, item, cache_base);
_generate_preview(texture, small_texture, item, cache_base, preview_metadata);
}
}
_preview_ready(item.path, texture, small_texture, item.id, item.function, item.userdata);
_preview_ready(item.path, 0, texture, small_texture, item.id, item.function, item.userdata, preview_metadata);
}
}

Expand All @@ -320,6 +315,22 @@ void EditorResourcePreview::_iterate() {
}
}

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) {
p_file->store_line(itos(p_thumbnail_size));
p_file->store_line(itos(p_has_small_texture));
p_file->store_line(itos(p_modified_time));
p_file->store_line(p_hash);
p_file->store_line(VariantUtilityFunctions::var_to_str(p_metadata).replace("\n", " "));
}

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) {
*r_thumbnail_size = p_file->get_line().to_int();
*r_has_small_texture = p_file->get_line().to_int();
*r_modified_time = p_file->get_line().to_int();
*r_hash = p_file->get_line();
*r_metadata = VariantUtilityFunctions::str_to_var(p_file->get_line());
}

void EditorResourcePreview::_thread() {
exited.clear();
while (!exit.is_set()) {
Expand Down
17 changes: 11 additions & 6 deletions editor/editor_resource_preview.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ class EditorResourcePreviewGenerator : public RefCounted {
static void _bind_methods();

GDVIRTUAL1RC(bool, _handles, String)
GDVIRTUAL2RC(Ref<Texture2D>, _generate, Ref<Resource>, Vector2i)
GDVIRTUAL2RC(Ref<Texture2D>, _generate_from_path, String, Vector2i)
GDVIRTUAL3RC(Ref<Texture2D>, _generate, Ref<Resource>, Vector2i, Dictionary)
GDVIRTUAL3RC(Ref<Texture2D>, _generate_from_path, String, Vector2i, Dictionary)
GDVIRTUAL0RC(bool, _generate_small_preview_automatically)
GDVIRTUAL0RC(bool, _can_generate_small_preview)

public:
virtual bool handles(const String &p_type) const;
virtual Ref<Texture2D> generate(const Ref<Resource> &p_from, const Size2 &p_size) const;
virtual Ref<Texture2D> generate_from_path(const String &p_path, const Size2 &p_size) const;
virtual Ref<Texture2D> generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const;
virtual Ref<Texture2D> generate_from_path(const String &p_path, const Size2 &p_size, Dictionary &p_metadata) const;

virtual bool generate_small_preview_automatically() const;
virtual bool can_generate_small_preview() const;
Expand Down Expand Up @@ -84,6 +84,7 @@ class EditorResourcePreview : public Node {
struct Item {
Ref<Texture2D> preview;
Ref<Texture2D> small_preview;
Dictionary preview_metadata;
int order = 0;
uint32_t last_hash = 0;
uint64_t modified_time = 0;
Expand All @@ -93,13 +94,16 @@ class EditorResourcePreview : public Node {

HashMap<String, Item> cache;

void _preview_ready(const String &p_str, const Ref<Texture2D> &p_texture, const Ref<Texture2D> &p_small_texture, ObjectID id, const StringName &p_func, const Variant &p_ud);
void _generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base);
void _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);
void _generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base, Dictionary &p_metadata);

static void _thread_func(void *ud);
void _thread();
void _iterate();

void _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);
void _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);

Vector<Ref<EditorResourcePreviewGenerator>> preview_generators;

protected:
Expand All @@ -112,6 +116,7 @@ class EditorResourcePreview : public Node {
// p_preview will be null if there was an error
void queue_resource_preview(const String &p_path, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata);
void queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata);
Variant get_preview_metadata(const String &p_path, const String &p_meta) const;

void add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator);
void remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator);
Expand Down
2 changes: 1 addition & 1 deletion editor/plugins/curve_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ bool CurvePreviewGenerator::handles(const String &p_type) const {
return p_type == "Curve";
}

Ref<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size) const {
Ref<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {
Ref<Curve> curve_ref = p_from;
ERR_FAIL_COND_V_MSG(curve_ref.is_null(), Ref<Texture2D>(), "It's not a reference to a valid Resource object.");
Curve &curve = **curve_ref;
Expand Down
2 changes: 1 addition & 1 deletion editor/plugins/curve_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class CurvePreviewGenerator : public EditorResourcePreviewGenerator {

public:
virtual bool handles(const String &p_type) const override;
virtual Ref<Texture2D> generate(const Ref<Resource> &p_from, const Size2 &p_size) const override;
virtual Ref<Texture2D> generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const override;
};

#endif // CURVE_EDITOR_PLUGIN_H
Loading

0 comments on commit 04317e9

Please sign in to comment.