Contraz / demosys-py

A light high performance modern OpenGL 3.3+ Python Framework https://demosys-py.readthedocs.io/
ISC License
64 stars 5 forks source link

Resource loading revamp? #33

Closed einarf closed 6 years ago

einarf commented 6 years ago

Currently the system is designed to schedule resources for loading returning an empty objects. This scheduling typically happens in Effect.__init__ and actions after loading happens in post_load().

For effects this is working perfectly fine, but built in features such as the text writer and mesh shaders for scenes makes this a bit more awkward. We don't know when the user instantiate these objects, before or after resource loading. TextRenderer2D is a good example where a user might need to dynamically create new instances during the programs execution. This issue was flagged by a user who dynamically generate labels for network nodes.

The solution so far has been to register resources at the module level so they are injected on import.

from demosys import resources

def on_load():
    resources.textures.get('demosys/text/VeraMono.png', cls=TextureArray, layers=190, create=True)
    resources.shaders.get('demosys/text/textwriter2d.glsl', create=True)
    resources.data.get('demosys/text/meta.json', create=True)

resources.on_load(on_load, priority=100)

class TextWriter2D(BaseText):

    def __init__(self, area, text_lines=None, aspect_ratio=1.0):
        self._texture = resources.textures.get('demosys/text/VeraMono.png', cls=TextureArray, layers=190, create=True)
        self._shader = resources.shaders.get('demosys/text/textwriter2d.glsl', create=True)
        self._config = resources.data.get('demosys/text/meta.json', create=True)

        resources.on_loaded(self._post_load, priority=100)

    def _post_load(self):
        """Parse font metadata after resources are loaded"""
        self._init(Meta(self._config.data))

The next next problem is that TextWriter2D also need to do initialization after resources are loaded, so it registers a callback to post_load. If the class was instantiated after resource loading, the post_load callback would not happen.

This was solved by immediately triggering the callback if resources were done loading because we know they were loaded since they where injected at import. Effects can also subscribe to this event, so we created a race condition: What post_load function would be called first? A priority system for callbacks where created to solve this. System files would use 100 while effect callbacks would use 0.

This gets even more confusing when extending classes registering callbacks in __init__. Both initializers should register a callback causing it to trigger twice and often before the child class is ready. Something should be figured out here.

An additional problem is that most opengl objects fetch the moderngl context in __init__. If for example an empty texture object is created before context creation we'll trigger an error, something that is completely unnecessary. We should not store the moderngl ctx variable on the instances itself. They should be properties fetching the context from the window itself.

@property
def ctx(self):
    return context.ctx()

This way we can create empty resource objects at any time and we don't have to care about import order.

Conclusion

I'm left with the feeling that resource loading issues can cause a lot of people to find the demosys hard to use. Most people will be fine when sticking to effects, but anything outside the ordinary can cause a lot of confusion because its designed to pre load all data before the draw loop starts. This is also not necessarily what everyone wants. It makes sense for what this package was originally made for, but doesn't lend itself well to more general purposes.

My thoughts so far is to allow resource loading even after the loading stage, but it will trigger a warning message. Each resource pool class will have a flag set when loading is done. When a resource is requested with get(..) after the loading stage, we immediately load and return the object followed by a warning message. This can also possibly be configurable in settings having the most liberal policy by default.

Also, finders currently returns the first matching resource found. This should be changed to the last resource found. Otherwise it will be impossible to override system resources and it matches how other other finder systems deal with resource overriding.

In addition we should use probably python Path objects internally.

This module offers classes representing filesystem paths with semantics appropriate for different operating systems

The resource system also needs proper tests and documentation.

stuaxo commented 6 years ago

Slightly OT: I wonder if this is a use for async / await ?

einarf commented 6 years ago

Not bad input.

I thought about that as well to speed up loading making each loading routine async through for example aiofiles. For the challenge describe above I don't think asyncio has any solution that wouldn't massively complicating things. I might to wrong, so feel free to correct me.

I've mainly been using asynchio with simpler problems reducing blocking time in a program or request by making IO async (files, http requests and whatnot), or in situations where I needed better control over long running operations yielding back control to the event loop on intervals + timeout support.

einarf commented 6 years ago

New branch for poking at things https://github.com/Contraz/demosys-py/tree/resource-loading

einarf commented 6 years ago

Looks like the reasonable way to go is to load the resources directly instead of scheduling loading to a separate referred loading stage. The downside is that we lose track of what resources should be loaded not being able to track loading progress feedback.

This can be solved by providing functionality to generate a resources.py file that would contain all the loaded resources during the run time of the project.

We remove the get() method for the more explicit load() method.

shaders.load("shader.glsl")
texture.load("texture.png")

We still keep track of all loaded resources in each respective pool in order to avoid loading a resource multiple times. This way we can do a pre-load stage with resources.pybefore initializing effects.

Another great advantage is that we no longer really need to make wrappers over moderngl types. Making people using moderngl objects directly is better for everyone and encourage us to contribute to moderngl features and docs even more.

1.0.5 can contain the loading change 2.0.0 should remove moderngl wrappers

einarf commented 6 years ago

Merged #37