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.