extends RefCounted
## Utility for saving and loading textures to memory or disk.
##
## [b]Example Usage:[/b]
## [codeblock]
## var store = TexturePackStore.new("user://textures")
## stare.max_packs_in_memory = 1
## var pack_a = store.add_textures([a, b, c])
## var pack_b = store.add_textures([d, e, f])
## var a_textures = pack_a.get_textures()
## store.cleanup()
## [/codeblock]
## The maximum Pack objects stored in ram. When this value is exceeded the
## oldest packs will be saved to disk.
var max_packs_in_memory := 10
## The maximum packs to save to disk before the oldest are deleted.
var max_packs_on_disk := 30
var _packs : Array
var _last_id := 0
var _save_threads : Dictionary
var _folder : String
class Pack extends RefCounted:
var textures : Array
var file_count : int
var saved : Array[String]
var save : Callable
var id : int
var thread : Thread
func _init(_textures,_id):
textures = _textures
file_count = textures.size()
id = _id
func get_textures() -> Array:
if textures.is_empty() and not saved.is_empty():
# Move textures back to memory.
for file in saved:
textures.append(ImageTexture.create_from_image(Image.load_from_file(file)))
print("Deleted and loaded '", file)
erase_from_disk()
return textures
func save_to_disk() -> void:
save.call(self)
func erase_from_disk() -> void:
if saved.is_empty():
return
for file in saved:
DirAccess.remove_absolute(file)
DirAccess.remove_absolute(saved.front().get_base_dir())
saved.clear()
func _notification(what):
if what == NOTIFICATION_PREDELETE and not saved.is_empty():
# Can't call functions here, see
# https://github.com/godotengine/godot/issues/31166.
# erase_from_disk()
for file in saved:
DirAccess.remove_absolute(file)
DirAccess.remove_absolute(saved.front().get_base_dir())
saved.clear()
func _init(path : String):
_folder = path
DirAccess.make_dir_recursive_absolute(path)
## Add a new list of textures and return a pack that can be used to load textures
## at a later point in time.
func add_textures(new_textures : Array) -> Pack:
var textures := []
for texture in new_textures:
if texture is ViewportTexture:
texture = ImageTexture.create_from_image(texture.get_image())
textures.append(texture)
var new_pack := Pack.new(textures, _last_id)
new_pack.save = self._save_pack
_packs.append(weakref(new_pack))
_last_id += 1
_save_to_disk_if_needed()
var on_disk := _get_packs(false)
if on_disk.size() > max_packs_on_disk:
# TODO: remove this, maybe make verbose?
print("too many saved")
# Don't free it, just clear the disk and memory space.
on_disk.front().get_ref().textures.clear()
on_disk.front().get_ref().erase_from_disk()
_packs.erase(on_disk.front())
return new_pack
func clear() -> void:
var path := ProjectSettings.globalize_path(_folder)
var dir := DirAccess.open(path)
if dir:
dir.remove(".")
if dir.dir_exists("."):
OS.move_to_trash(path)
func _get_packs(in_memory : bool) -> Array:
var packs := []
for pack in _packs:
if pack.get_ref():
var pack_in_memory : bool = (pack.get_ref() as Pack).saved.is_empty()\
and not pack.get_ref().id in _save_threads
if in_memory == pack_in_memory:
packs.append(pack)
return packs
func _save_to_disk_if_needed() -> void:
var packs_in_memory := _get_packs(true)
if packs_in_memory.size() > max_packs_in_memory:
packs_in_memory.front().get_ref().save_to_disk()
# Function called by a pack so threads can be handled here, to avoid packs with
# unfinished threads being freed.
func _save_pack(pack : Pack):
var thread := Thread.new()
_save_threads[pack.id] = thread
thread.start(_threaded_save_to_disk.bind(pack))
func _threaded_save_to_disk(pack : Pack) -> void:
var base := _folder.path_join(str(pack.id))
DirAccess.make_dir_recursive_absolute(base)
for texture_num in pack.textures.size():
# TODO: remove this or make verbose
var path := base.path_join(str(texture_num) + ".png")
print("Saved to ", path)
pack.textures[texture_num].get_image().save_png(path)
pack.saved.append(path)
pack.textures.clear()
call_deferred("_save_thread_completed", pack.id)
func _save_thread_completed(pack_for : int):
_save_threads[pack_for].wait_to_finish()
_save_threads.erase(pack_for)