godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
89.18k stars 20.22k forks source link

ResourceLoader doesn't seem to cache loaded assets even with CACHE_MODE_REUSE #90344

Open xynanlee opened 5 months ago

xynanlee commented 5 months ago

Tested versions

System information

Godot v4.1.stable - Windows 10.0.19045 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 2070 SUPER (NVIDIA; 31.0.15.3623) - AMD Ryzen 7 3700X 8-Core Processor (16 Threads)

Issue description

The issue

ResourceLoader.load doesn't seem to cache the asset within ResourceLoader itself even with CACHE_MODE_REUSE.

The code that prints false in ResourceLoader.has_cached:

func _non_cached_load():
    ResourceLoader.load("res://TestResource.tres", "", ResourceLoader.CACHE_MODE_REUSE)
    print(ResourceLoader.has_cached("res://TestResource.tres"))

The code below prints true in ResourceLoader.has_cached:

func _stack_cached_load():
    var x = ResourceLoader.load("res://TestResource.tres", "", ResourceLoader.CACHE_MODE_REUSE)
    print(ResourceLoader.has_cached("res://TestResource.tres"))

_stack_cached_load() works because x is still within scope in the function. The asset will fall out of scope as soon as it code execution exits the function.

Expected behavior

_non_cached_load() should return true.

Steps to reproduce

  1. Create a resource in the project.
  2. Create a function similar to what is described in _non_cached_load.
  3. The return is false.

Minimal reproduction project (MRP)

MRPLoadCacheBug.zip

KoBeWi commented 5 months ago

ResourceCache works like WeakRef - if a resource is referenced somewhere, you can get it from cache. If it isn't, it will be loaded again. In your first example you don't store the Resource anywhere, so it gets unloaded. If all resources would stay in cache forever you'd run out of memory quickly.

Seems like the description of has_cached() could clarify that.

xynanlee commented 5 months ago

ResourceCache works like WeakRef - if a resource is referenced somewhere, you can get it from cache. If it isn't, it will be loaded again. In your first example you don't store the Resource anywhere, so it gets unloaded. If all resources would stay in cache forever you'd run out of memory quickly.

Seems like the description of has_cached() could clarify that.

Alright, I think the documentation would need to further clarify on what the cache modes do, I currently have the impression of:

  1. CACHE_MODE_IGNORE: the asset is not cached anywhere, hence I relate the issue with this very cache mode.
  2. CACHE_MODE_REUSE: the asset is loaded and cached internally within ResourceLoader therefore I would expect _non_cached_load to return true without my intervention to store the loaded asset. This behavior is exactly how Unreal handles their AssetManager and PrimaryDataAsset.
  3. CACHE_MODULE_REPLACE: the asset is loaded and cached internally within ResourceLoader regardless of the asset has been previously loaded or not. I'm not sure what this cache mode actually do. Since the ResourceCache works like a WeakRef and the loaded assets are fallen out of scope without manual storage. Then it is kinda like CACHE_MODE_IGNORE, because they are not cached internally anyway.

Overall, my impression of the system is the ResourceLoader will own the assets in the form of hard references as a centralized loading mechanism and the loaded assets will return as a WeakRef. This architecture can also synergize with the Export WeakRef feature proposal. Since, I'm looking into ways to break cyclic reference.

I'll look forward to listen more on how should I use the ResourceLoader because I think my current impression of it has used it outside of its intended usage.

Edit: Currently I feel like the system works like CACHE_MODE_IGNORE, regardless of cache modes if I don't do manual caching myself.

KoBeWi commented 5 months ago

ResourceCache is more like database or registry. When you load a Resource, as long as something references it (it's RefCounted, which means the instance is reference counted), loading the same resource will return the same instance. If the instance is unloaded, the cache entry for its path will be removed and you'll need to load it again.

If you want the Resource to persist, you need to store it in some more persistent place, e.g. node that exists while your game is running (usually autoload).

Cache modes are explained here: https://docs.godotengine.org/en/latest/classes/class_resourceloader.html#enum-resourceloader-cachemode. The stable docs are a bit outdated and the CACHE_MODE_REPLACE works differently in 4.3 (in more expected way).

xynanlee commented 5 months ago

I think the documentation can be further improved to emphasize on the having to store the loaded assets manually by ourselves since the documentation currently implies that the loaded assets are owned and managed internally.