Skip to content

Add the ability to see and unload Resources loaded by ResourceLoader.load_threaded_request() #86603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

AlexOtsuka
Copy link
Contributor

In response to my own needs and inspired by this proposal, I've added 2 new methods to ResourceLoader:

  • ResourceLoader.load_threaded_cancel() is the direct counterpart to ResourceLoader.load_threaded_request()
  • ResourceLoader.get_requested_paths() returns an Array listing all currently requested Resources

Without reiterating the aforementioned thread, I believe those are core features needed to properly manage background loading of Resources in many different contexts. This will let users track and cancel Resources they haven't ended up needing and will work out of the box in existing projects.

Looking forward to your feedback and criticism.

Comment on lines 452 to 456
thread_load_mutex.lock();
for (KeyValue<String, ResourceLoader::LoadToken *> &kvp : user_load_tokens) {
requested_paths.push_back(kvp.key);
}
thread_load_mutex.unlock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
thread_load_mutex.lock();
for (KeyValue<String, ResourceLoader::LoadToken *> &kvp : user_load_tokens) {
requested_paths.push_back(kvp.key);
}
thread_load_mutex.unlock();
MutexLock lock(thread_load_mutex);
for (KeyValue<String, ResourceLoader::LoadToken *> &kvp : user_load_tokens) {
requested_paths.push_back(kvp.key);
}

Safer and preferred usage with trivial locking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reviewing my PR, I've applied your suggestion!

@AThousandShips
Copy link
Member

Welcome to contributing! Will take a deeper dive into this when I have the time 🙂

@AlexOtsuka AlexOtsuka force-pushed the cancellable-threaded-loading-requests branch 2 times, most recently from b1cde5b to 6342624 Compare December 29, 2023 14:23
@@ -420,6 +420,44 @@ Error ResourceLoader::load_threaded_request(const String &p_path, const String &
}
}

void ResourceLoader::load_threaded_cancel(const String &p_path) {
Copy link
Member

@RandomShaper RandomShaper Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding correctly, this is a load_threaded_get() without honoring reference counting. In that case, I'd suggest refactoring the code so both functions share a common implementation. There would be a new argument to ignore the ref-count and instead relentessly clear it. Moreover, since it's not actually a proper cancel, I'd name this one load_threaded_forget(). But this leads to the question, wouldn't it be even cooler to have a proper load_threaded_cancel() that actually stops the load? Nonetheless, that would be much harder to implement and could be an addition for the future. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello RandomShaper,

Thank you for your feedback. I have read your post and agree with pretty much the entirety of your thoughts. I'm sick right now, but I will get back into this and give a more comprehensive answer.

Refactoring the code is something I had been avoiding as not to break previously existing code, but is obviously worth considering if such a change is positively welcomed by the community.

When it comes to cancelling the actual loading process from load_threaded_request(), I will have to dive deeper into its current implementation and figure out how to do this in a thread-safe manner.

Will spend more time on this when my brain works again.

@AlexOtsuka AlexOtsuka force-pushed the cancellable-threaded-loading-requests branch 2 times, most recently from d8238f0 to d262e83 Compare January 17, 2024 04:38
@AlexOtsuka AlexOtsuka force-pushed the cancellable-threaded-loading-requests branch from d262e83 to 8e4d560 Compare January 17, 2024 04:50
@AlexOtsuka
Copy link
Contributor Author

AlexOtsuka commented Jan 17, 2024

I've made a few changes following feedback from @RandomShaper and after acquiring a deeper understanding of ResourceLoader.

  • loaded_threaded_cancel() had been renamed to loaded_threaded_forget() as suggested. Especially with the changes I've now made to it, it is a much more appropriate name for the method.
  • loaded_threaded_forget() is now non-blocking. Instead of waiting to unload resources that are currently being requested, it only deals with resources that have already fully finished loading.
  • Because of this change, get_requested_paths() now only returns paths to resources that have finished loading after being requested, as in resources that are ready to be instantly disposed of by calling load_threaded_forget().

I have tried implemented a form of load_threaded_cancel() acting on currently loading resources while being non-blocking at the same time. It basically revolved around adding a task to WorkerThreadPool and waiting for the loading to finish from inside another thread, and while it worked reasonably well, I couldn't find a way to prevent it from crashing when calling load_threaded_get() on that same Resource before that task was completed. While this is definitely implementable, it'd require modifying other parts of resource_loader.cpp, and the functionality doesn't seem worthwhile as it would still not be capable of immediately aborting load_threaded_request(). Which as far as I can tell, cannot easily be done.

Because load_threaded_forget() has been made so much simpler, refactoring the code doesn't seem necessary anymore. load_threaded_get() goes through a number of checks that are unnecessary in load_threaded_forget() (we don't care if the resource is valid) and _load_complete() is already being used by both threaded and non-threaded loading methods.

I hadn't actually realized before working on this that load_threaded_request() increased the reference count for the LoadToken every time it was called on the same resource (I assumed the call was simply ignored). This actually increases the need for load_threaded_forget(), as there's no clear way to keep track of how many calls to load_threaded_request() were previously made and how many calls to load_threaded_get() would be necessary in order to flush the resource out of the cache.

Thank you for your interest so far, and please inform me of any feedback or criticism you may have.

@greenpixels
Copy link

@AlexOtsuka
Thanks for working on this! I am very interested in this functionality - especially in load_threaded_cancel.
Any chance you made progress on this since then?

If not, could you elaborate on the challenges of implementing load_threaded_get. Being able to cancel an on-going loading operation is something I needed very often in my projects - and I don't seem to be the only one. And to my knowledge, there is no valid workaround in Userland for this - only in-engine.

A great example would be to cancel the loading of large animations that are stuffing the VRAM. Currently you'd need to call load_threaded_get to claim the resource and have it discarded after, which would hang the main-process.

@AlexOtsuka
Copy link
Contributor Author

AlexOtsuka commented Mar 10, 2025

Hey @greenpixels,

Thank you for your interest in this PR! I agree with you that this is a much needed feature. So far if I remember correctly, the closest thing to it that could be done was to wait for it to finish requesting the resource, load it, lose all references to it and make sure it's not in the cache anymore. Actually interrupting the threaded loading process was challenging as I couldn't figure out a way to interact with the actual task and stop it in a clean thread-safe manner.

I haven't looked at this in a while, but will actually dive into it again tonight to remind myself of how this all worked and what could be done to actually implement this functionality to work in the way I had originally hoped. Maybe this time, I can make further progress, and if not, post a detailed breakdown of the issues at hand.

Edit (mistakenly closed the PR trying to make this edit whoops): I think a user-side workaround that I had some success with was to load and discard the requested resource inside a Thread as to avoid hanging the main process, but it's a little janky.

@AlexOtsuka AlexOtsuka closed this Mar 10, 2025
@AlexOtsuka AlexOtsuka reopened this Mar 10, 2025
@AlexOtsuka
Copy link
Contributor Author

Alright, sorry it took so long but here's what I got. Please keep in mind I haven't written any of this code and this is all reverse-engineering. Also, props to the team for improving the code quality so much in just a year!

  • The general principle is that whenever you request a Resource, a LoadToken and a ThreadLoadTask are generated in order to manage and keep track of whatever you're loading.
  • As far as I can tell, interrupting a task before it ends is never done in the entire program. Even when we aggressively clear whatever remains in the memory during shutdown, we still wait for all threaded loading to complete before we actually discard the resource, load task and token. This is how ResourceLoader::clear_thread_load_tasks() works, and why it uses a delay function to wait for all thread load tasks to fully complete, as seen here:

https://github.com/godotengine/godot/blob/2303ce843a362cf5497ca3336451de23793eb711/core/io/resource_loader.cpp#L1388C2-L1407C3

  • Whenever a new threaded load is initiated, it's added to the WorkerThreadPool. As far as I know, there isn't currently a way to forcefully interrupt a Task inside the pool. Ideally, this would be implemented with something akin to C++ 20's std::stop_token (https://en.cppreference.com/w/cpp/thread/stop_token) but Godot's code is based on C++ 17 and this is probably very tricky to implement in a robust, thread-safe manner.

That's where I'm at engine-side. I don't think it's impossible, but it's definitely a ton of work.

Now when it comes to user-side implementations. I actually had a mostly completed demo for the last iteration of this PR. I modified it to run on normal Godot with a user-side wrapper class that mostly does the same things. Feel free to check it out here : https://github.com/AlexOtsuka/background-loading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants