Advertisement

Each system loading its own assets VS. one asset managing system

Started by July 26, 2022 09:16 PM
7 comments, last by DaveF 2 years, 3 months ago

I have few diferrent systems that loosely follow the ECS principle. Components are stored in a level, which every system has acces to. The level should be the only thing which they all have in common. Each system runs on its own thread and has something like a messaging system - If I call one of the systems methods (from any thread), it doesn't execute immediately, but instead add the necessary functionality into some kind of command queue which gets executed at the systems own thread later.

I tried to use something like an asset managing system, but it just made things messier. The problem is, that when asset managing system needs to load a texture, it has to notify the rendering system. It is because I use opengl to both load the texture into gpu and render it. Opengl can only be operated within a single thread thus within a single system.

So, imagine a rendering system wants to render a sprite component. It notifies the asset manager to get the correct texture, the asset manager realises that it isn't loaded yet and sends a request back to the rendering system to load it.

This just made my systems dependent on each other, which i didn't want.

Would it be better to abolish the asset managing system and just let each system to load its own data when it needs it?

DaveF said:
Would it be better to abolish the asset managing system and just let each system to load its own data when it needs it?

Definitely not. Assets belonging to different systems are still similar objects (e.g. dressed up OpenGL textures) that are meant to be loaded in the same way (e.g. specific image file formats) from the same sources (e,g. layered main archives, mods, loose files): avoid introducing repetitions and incoherence.

From your description, the current solution seems messy because you deal with concurrency very badly (trying to wait and block when you can't), not because of a perfectly natural dependency between systems. Do you have enough memory to load assets in advance and execute proper rendering only when all assets are ready?

Omae Wa Mou Shindeiru

Advertisement

LorenzoGatti said:

From your description, the current solution seems messy because you deal with concurrency very badly (trying to wait and block when you can't)

Well, I am neither blocking or waiting (If I understand the meaning of those therms correctly), the rendering system doesn't wait until it gets the correct texture, it just renders a placeholder.

My only problem is, that system A sends system B a request, and the system B sends it back to A. Which made me think that it could be easier to just remove B entirely.

LorenzoGatti said:

Do you have enough memory to load assets in advance and execute proper rendering only when all assets are ready?

Yes I do, but I would love to have an option to unload and load assets on the fly.

@undefined

DaveF said:
My only problem is, that system A sends system B a request, and the system B sends it back to A. Which made me think that it could be easier to just remove B entirely.

I don't think so. What you described was a mixing of interests and unnecessary coupling, not a cyclic dependency.

Components shouldn't know anything at all about rendering or OpenGL. Components know about components, and if they have any references to anything about rendering, that is a reference to another object that knows about rendering. Each should have a single responsibility, those responsibilities shouldn't be mixed together.

A common conceptual pattern for resources is the pattern of a store, a proxy, a cache, and a loader. The component tells the store “I need a drawable thing” and it immediately returns an object which is a proxy for the real resource. The resource may or may not be loaded at any time, where it lives in the resource cache. There is a loader that can load and unload items from the cache as needed. In major game engines the entire process happens invisibly, behind your back.

Your component requests from the conceptual store “please give me a sprite” and the store immediately generates a proxy object and returns it, “here is your sprite!” The store may have an actual sprite texture available for use instantly, or it may have queued the work to load the sprite texture, but either way nothing is blocked.

You can do this all the time in game engines. For example in Unreal, you request a UStaticMesh. You get the C++ object back instantly. But internally it is a UStreamableRenderAsset, the actual mesh may not be streamed yet at all, your only guarantee is that gets created with enough information to be usable in the gameplay logic. The streamable object internally has information about if it has streaming updates pending, if specific levels are available, the current LOD level and max LOD levels, which are resident already, and more. You have a resource store where you requested the resource, the UStaticMesh is a proxy object to the actual resource, there is a cache of actual streamed assets living somewhere, and there is a loader that manages loading and unloading of resources.

DaveF said:
The problem is, that when asset managing system needs to load a texture, it has to notify the rendering system. It is because I use opengl to both load the texture into gpu and render it. Opengl can only be operated within a single thread thus within a single system. So, imagine a rendering system wants to render a sprite component. It notifies the asset manager to get the correct texture, the asset manager realises that it isn't loaded yet and sends a request back to the rendering system to load it.

The conceptual proxy object above maintains all the information, all the time. It can say “I'm a texture” so it knows all about the texture, but the actual texture may or may not be loaded. The conceptual object may be part of your environment or world, but that doesn't mean the texture is loaded nor ready to be rendered.

Your conceptual cache object can have many different instances, and that's the thing that your rendering system will render. A cache object can be used for many different proxy objects, especially if you've implemented rendering object instancing.

It is certainly an area where the systems are closely linked, but each has a separate responsibility. The gameplay code only needs to know about the proxy resource, it doesn't matter to gameplay if the resource actually exists, such as if an object is culled and off screen, all the gameplay cares about is that it can manipulate the meta-information about the resource. The rendering system needs to know details about the instances that are actually loaded into the cache and ready to render, and needs to have collective information for better batching work in the render. The loader needs to know about what resources have been destroyed and can have their memory reclaimed, or what objects have been created and need to have their final resource contents loaded for display. And the conceptual store serves as the hub for all the information, where gameplay can request new resources and have something back instantly, and where rendering can access the concrete loaded resources.

The decoupling also allows additional behind-the-scenes changes. When an object is near or far the rendering system can tell the store it needs a different level of detail; if it is in the cache the store can modify which resource to use and adjust the proxy index to refer to it; if it isn't already loaded in the cache the loader can load it and the store can do the modification when the load is complete. The gameplay's object of the conceptual resource proxy doesn't need to know anything about which level of detail is being used, although it has the capacity to discover which linked resource is being used if needed.

Thank you very much for your beautiful explanation. I will definitely use this knowledge later. But my current problem is, that thanks to an opengl limitation, the code responsible for rendering has to run on the same thread as the code responsible for loading textures, shaders etc.

I have 3 options:

1)Make the Rendering system run on the same thread as the Asset managing system. This would mean, that loading assets and rendering cannot be done concurrently.

2) Rendering System and Asset managing system would be on separate threads. Renderering system wants a texture, asset managing systems gives it a proxy texture. But when the time comes to load the actual texture, the asset managing system has to send message to the renderering system to call the necessary opengl functions on its thread. So it would actually be the Renderering system that loads the texture.

3) Ditch the asset managing system altogether - after what you have told me, this option is the worst thing to do.

DaveF said:
But my current problem is, that thanks to an opengl limitation, the code responsible for rendering has to run on the same thread as the code responsible for loading textures, shaders etc.

This is an old limitation, and a reason for shifting to Vulkan. It gets more complexity though, and ultimately still boils down to one driver and hardware bus so if you can keep it busy a single thread will do. Either way I don't think you're ready to deal with that extra complexity based on the posts.

You have more options than that.

The most common is to have a dedicated rendering thread, and use thread-friendly libraries to communicate between them as needed. Depending on how much data you have shifting around it can range from simple messaging and lockless queues to a complex interplay of a task management system.

In what you described for your project, some lockless queues where work gets added externally and the rendering system pulls it out is probably the better option. The loading system gets the data ready and shuffles it over to the rendering thread. When the rendering thread gets the assets up to the driver/card it communicates the ID back over to the managing system, and in later draw calls the rendering thread can use the texture ID in future commands.

Advertisement

DaveF said:
But my current problem is, that thanks to an opengl limitation, the code responsible for rendering has to run on the same thread as the code responsible for loading textures, shaders etc.

I have never done it, but I believe you can create 2 contexts that share the same buffers/textures/etc and load asynchronously into one context, then use the resources in the other context for rendering once they are ready.

@aressera Thank you! That sounds like the best solution. I will definitely try it. Thank you.

This topic is closed to new replies.

Advertisement