Skip to content
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

VideoCommon: migrate texture packs to use the asset loader system #11681

Merged
merged 4 commits into from Jun 8, 2023

Conversation

iwubcode
Copy link
Contributor

@iwubcode iwubcode commented Mar 22, 2023

This moves high resolution textures to be loaded as assets by the asset loader system. This makes the high resolution textures load in an asynchronous manner and also allows for them to be reloaded automatically:

asset_management_auto_reload

High resolution textures did have some particular behaviors that didn't fit very well in the asset loader system, I've listed them below.

Change: The OSD messages about all textures being loaded was removed.
Response: I know that this message is useful for debugging. This doesn't make sense for the asset loader though because an asset can be loaded at any time (not just on start up). My idea is to add an imgui window that lists a count of every asset type loaded. I haven't done this yet. If this is a blocker, I can implement it in a separate review before merging this.

Change: High resolution textures no longer block the game when they need to load. This eliminates stutter at the cost of textures "popping in".
Response: On a higher fidelity machine, this doesn't seem distracting and all textures seem to be loaded just as quickly as on master. At least one or two users I've spoken with said they'd prefer pop-in over stutter if given the choice. And at least one other emulator offers this option (while also mentioning this is the preferred approach) so moving to this wouldn't surprise users.

Change: Previously, if the high resolution textures exceeded the memory limit Dolphin calculated, high resolution textures would be turned off completely and aborted. Now the system will just reject future assets, logging that the memory cap has been reached.
Response: I think the previous solution was a poor decision and think that users which are memory capped would rather see some of the textures then none at all. In the future a smarter memory management system would possibly alleviate this.

Change: Previously, mip textures were more flexible and could be anywhere in the texture pack.
Response: This made sense given the solution but is now difficult to do. I could introduce a separate library system just for high-resolution textures but in practice most packs I've seen keep their mip textures next to the main textures or are using DDS textures with the mips included. Most packs seem to use the same file type for the mip as they do the main texture too. The odd packs that don't should be updated to one of the aforementioned solutions.


TODO:

  • Support the "_mip" textures
  • Cleanup DynamicInputTexture logic to no longer force the texture cache to reload when input changes occur

@iwubcode iwubcode marked this pull request as draft March 22, 2023 04:56
@MayImilae
Copy link
Contributor

High resolution textures no longer block the game when they need to load. This eliminates stutter at the cost of textures "popping in". On a higher fidelity machine, this doesn't seem distracting and all textures seem to be loaded just as quickly as on master

That is a significant behavior change. I assume this can still be avoided by preloading?

@iwubcode
Copy link
Contributor Author

That is a significant behavior change. I assume this can still be avoided by preloading?

Yes, preloading is still supported.

@iwubcode

This comment was marked as outdated.

@iwubcode iwubcode changed the title VideoCommon: add support for loading generic assets, support auto-reloading VideoCommon: migrate texture packs to use the asset loader system Jun 3, 2023
@iwubcode iwubcode marked this pull request as ready for review June 3, 2023 18:25
@AdmiralCurtiss
Copy link
Contributor

AdmiralCurtiss commented Jun 4, 2023

Okay, this is a pretty unfamiliar corner of the code for me, so let me know if I got the logic right.

  • On game boot (and when a high-res texture config option is changed), HiresTexture::Update() is called.
    • This enumerates all textures that exist in the ./Load/Textures subfolders matching the currently running game and 'registers' them in the s_file_library and in s_hires_texture_id_to_arbmipmap,
    • and possibly preloads it (by creating a HiresTexture and placing it in s_hires_texture_cache).
  • When a game then encounters a texture, HiresTexture::Search() is called for each texture.
    • This first checks the s_hires_texture_cache for a matching texture and if yes returns it,
    • otherwise it checks the s_hires_texture_id_to_arbmipmap for if there is a registered non-loaded texture with that name and if yes loads it (by creating a HiresTexture and placing it in s_hires_texture_cache).

Both of these cases don't actually load the texture into memory yet, but only create an unfilled Asset object that will then be asynchonously filled by a background thread.

Okay, I think that's right so far. But then I'm not entirely sure what happens next. Whenever a texture is then rendered by the game, TextureCacheBase::Load() is called and that does... what, exactly? I'm not really sure how the dynamic reloading works correctly at all. The m_last_write_time of the linked asset is never set to anything, so the last_asset_write_time > entry.linked_asset.m_last_write_time check always returns true, so the asset is reloaded every single time it's rendered? Surely that's not the intent, right? Wait, it is set with that entry->linked_asset = std::move(cached_texture_asset), missed that one. But even still, this check looks pretty race-y -- the m_last_loaded_time on the CustomAsset is set from a the background loading thread and is not protected by anything. Something is definitely wrong here.

Also where exactly does it detect when a file on disk changes? I feel like I'm missing that part completely. Ah I think I found it, it's in CustomAssetLoader::Init().


But anyway, that aside, assuming the reloading did work correctly, that still means:

  • Regardless of if you prefetch or not,
    • Any new texture that did not exist when HiresTexture::Update() was called will not be detected and loaded until that function is called again.
      • Is this desired? Should there be an option for force-rescanning the texture directories for when you add a new texture while the game is running?
    • But, all textures that did already exist will be automatically updated when the file on disk changes.
    • You also incur a slight delay on boot even without prefetching as the files are enumerated. (Probably negligible, but worth noting.)

One other thing I noticed: Does the wildcard feature actually work properly when not prefetching here? I don't think it does; not only are cached wildcard textures preferred to uncached non-wildcard textures, the s_hires_texture_id_to_arbmipmap.find(full_name) check will never match any wildcarded texture name because full_name does not contain wildcards.

You also never clear s_hires_texture_id_to_arbmipmap anywhere.

@iwubcode iwubcode marked this pull request as draft June 5, 2023 01:38
@AdmiralCurtiss
Copy link
Contributor

AdmiralCurtiss commented Jun 5, 2023

fwiw I think if you just make CustomAsset::m_last_loaded_time an std::atomic<TimeType> then the threading logic around that should be okay, assuming I understand the sequencing guarantees about that correctly.

@iwubcode iwubcode force-pushed the asset_management branch 2 times, most recently from aaa5cfe to 8ebc832 Compare June 5, 2023 04:24
@iwubcode
Copy link
Contributor Author

iwubcode commented Jun 5, 2023

First of all, thank you so much Admiral for reviewing this! Especially when it's not your usual stomping grounds :)

Okay, I think that's right so far

Yes, you are!

Whenever a texture is then rendered by the game, TextureCacheBase::Load() is called and that does... what, exactly?

If the texture doesn't exist in cache, it will create a texture. As part of this creation process, if high resolution textures are enabled, we try and load an asset as a "linked_asset", which is both the asset and a cached load time.

I'm not really sure how the dynamic reloading works correctly at all.

Next time the game tries to access that asset, we check to see if our cached time is older than our asset's load time and if so we reload the asset. There was a bug here...which I'll talk about after all this.

But even still, this check looks pretty race-y -- the m_last_loaded_time on the CustomAsset is set from a the background loading thread and is not protected by anything. Something is definitely wrong here.

Yes, this was a great catch. You're right I could have used a atomic. I opted for a lock instead but honestly never know when I should prefer one over the other. In looking over your statement and viewing the code, I found a few places that could explode in the right conditions. I opened #11891 with a number of changes.

Any new texture that did not exist when HiresTexture::Update() was called will not be detected and loaded until that function is called again. Is this desired? Should there be an option for force-rescanning the texture directories for when you add a new texture while the game is running?

This is actually an "issue" in the current logic too. From my understanding, most people will just check and uncheck the "Load Textures" box to force a refresh. It is klunky but users are used to it. I was planning on reworking this with a mythical editor.

But, all textures that did already exist will be automatically updated when the file on disk changes.

Yes.

You also incur a slight delay on boot even without prefetching as the files are enumerated. (Probably negligible, but worth noting.)

That is a good point, hmm.

One other thing I noticed: Does the wildcard feature actually work properly when not prefetching here? I don't think it does; not only are cached wildcard textures preferred to uncached non-wildcard textures, the s_hires_texture_id_to_arbmipmap.find(full_name) check will never match any wildcarded texture name because full_name does not contain wildcards.

Wildcard worked with prefetch it was broken without it. I fixed that. Thank you for catching that!

You also never clear s_hires_texture_id_to_arbmipmap anywhere.

Thank you for that as well! Updated.

@iwubcode
Copy link
Contributor Author

iwubcode commented Jun 5, 2023

Ok, now to the bug. I noticed that the original code wrapped the cache calls with this:

if (g_ActiveConfig.bCacheHiresTextures)

I was only doing that in the initial load. This bCacheHiresTextures is a bit of a poorly named variable, it actually is whether you've turned on prefetching. Should we not be caching when prefetching is off? Seems odd to me but that's what the original code had so...

Regardless, once I put that in, I noticed none of my textures were loading with prefetch off. Why? Well first was that I was destroying the last reference to the asset (because the asset wasn't stored in the cache anymore!) before loading the new texture which caused things to reset. But even after fixing that, I still saw the issue. Turns out the game I was testing was leveraging the tmem caching!

I now provide a force_reload flag to force the reloading when the asset associated with a texture changes.

@iwubcode iwubcode marked this pull request as ready for review June 5, 2023 22:02
Copy link
Contributor

@AdmiralCurtiss AdmiralCurtiss left a comment

Choose a reason for hiding this comment

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

This looks sane to me now. Someone should probably still test a few edge cases, maybe I'll do that later.

@AdmiralCurtiss
Copy link
Contributor

Okay, tested this a bit. Seems to work fine but I found a crash when a texture is renamed/removed while the game is running -- you're using the throwing std::filesystem variants.

@AdmiralCurtiss
Copy link
Contributor

Oh, that piece of code was in another PR... well whatever, I'll just merge this and then fixup in a different PR.

@AdmiralCurtiss AdmiralCurtiss merged commit 7845fb0 into dolphin-emu:master Jun 8, 2023
14 checks passed
@iwubcode iwubcode deleted the asset_management branch June 8, 2023 20:22
@AdmiralCurtiss
Copy link
Contributor

AdmiralCurtiss commented Jun 8, 2023

Specifically it's the two calls in DirectFilesystemAssetLibrary to std::filesystem::last_write_time(), you need to pass the std::error_code parameter and then handle the case when that returns an error.

https://en.cppreference.com/w/cpp/filesystem/last_write_time

@iwubcode
Copy link
Contributor Author

iwubcode commented Jun 8, 2023

Thanks @AdmiralCurtiss ! I didn't even think of that. I can open a review in a few hours.

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