vkd3d: Introduce cache enumeration
This cache function is getting into more controversial territory. This is introducing a function for enumerating cache contents and (ab)uses a cleanup function to be the first user. While cleaning up cache contents could be dealt with in other ways - more on that later - we want an enumeration function regardless.
Why do we need it: Our input data (e.g. cache code) to output isn't always about producing static data like SPIR-V code. Instead, we have to invoke external code and can't (reliably) feed it a blob of data to get a usable shader fast. Because this external code can be slow (e.g., SPIR-V -> hw blob translation) we have to invoke it before the game creates d3d pipelines.
IOW, if d3d bc to SPIR-V was our bottleneck we wouldn't need to enumerate. We could just look up the SPIR-V code when the d3d game creates a pipeline. If we could reliably (across macos versions / mesa versions and possibly across hardware feed a cached blob to Vulkan and get a working pipeline fast we could do so once the game tells us what it needs.
Considered alternatives:
-
Make it the cache user's problem: Vkd3d could maintain a list of known keys and store it in the cache with a static key. This would eliminate the need for an enumeration function in the cache API. However, it would just shift the problem and in my mind make it uglier. We'd need a way to prevent eviction of this magic object of objects. We'd need to handle eviction of keys listed in that object of objects (fairly easy). But we'd also need a way to prevent duplicating cache listings (necessitating a fancy data structure in the object of objects like a tree). We'd need to synchronize the read-modify-write update pattern. So the user of the cache would need its own kind of cache to maintain a list of keys.
-
Have cache_enumerate() return an array of key-value pointers instead of a callback: I didn't like the extra allocation and iterating over the cache twice (once to generate the array and once for the caller to actually consume it. A possible advantage is that we could allow writes to the cache while the caller is consuming the list if we make sure that we don't free any allocations (i.e. disable eviction temporarily) or move them (e.g. writes that replace existing values).
-
enumerate(), step() and end() functions, with the equivalent of a struct rb_entry as a cursor stored in the cache. The problem is that we want enumerate to be reentrant if two devices are created at the same time. That could be solved by having the caller provide memory for said struct rb_entry. But IMHO the call stack does that just as well without creating a mess of communicating the size of the cursor structure to the caller.
Re the first user, the cleanup function: We may at some point in the future add the ability to provide a destroy callback for clients that store more than just inert data in the cache. That's orthogonal to the need of an enumeration function. For now I am happy with requiring clients that have special destruction needs to disable eviction (which so far is unimplemented anyway) and destroying contents manually.