nir / jupylet

Python game programming in Jupyter notebooks.
https://jupylet.readthedocs.io
BSD 2-Clause "Simplified" License
230 stars 22 forks source link

how to run an App() in a separate thread? #32

Closed cyberic99 closed 1 year ago

cyberic99 commented 3 years ago

Hi,

I'd like to run an (headless) App in a separate thread.

I'd like to render some shaders, and use the framebuffer into another part of my application.

I tried this simple code:

#!/usr/bin/python3

import time
from jupylet.app import App

if __name__ == '__main__':

    def create_and_run_app():
        app = App(width=512, height=512, quality=100, fullscreen=False, mode="hidden")  #, log_level=logging.INFO)
        app.start()

    import _thread
    _thread.start_new_thread(create_and_run_app, ())

    time.sleep(10)

but I get this error:

Exception ignored in thread started by: <function create_and_run_app at 0x7f807b0060d0>
Traceback (most recent call last):
  File "p.py", line 9, in create_and_run_app
    app = App(width=512, height=512, quality=100, fullscreen=False, mode="hidden")  #, log_level=logging.INFO)
  File "/home/eric/tmp/jupylet/jupylet/app.py", line 184, in __init__
    self.window = window_cls(size=(width, height), **conf)
  File "/home/eric/tmp/jupylet/jupylet/event.py", line 81, in __init__
    self.init_mgl_context()
  File "/home/eric/tmp/jupylet/jupylet/event.py", line 103, in init_mgl_context
    self._ctx = moderngl.create_standalone_context(
  File "/home/eric/.local/lib/python3.9/site-packages/moderngl/context.py", line 1672, in create_standalone_context
    raise ValueError('Requested OpenGL version {}, got version {}'.format(
ValueError: Requested OpenGL version 330, got version 0

I also tried to use mode="auto" and app.run(), but I get:

Exception ignored in thread started by: <function create_and_run_app at 0x7f34e3fc30d0>
Traceback (most recent call last):
  File "p.py", line 10, in create_and_run_app
    app.run()
  File "/home/eric/tmp/jupylet/jupylet/app.py", line 333, in run
    loop = asyncio.get_event_loop()
  File "/usr/lib/python3.9/asyncio/events.py", line 642, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Dummy-1'.

Is there a way to call a render iteration x times per second, or in a separate thread?

Thank you

nir commented 3 years ago

Hi, under the hood Jupylet and the App object employ Python's async programming. It is a programming paradigm that is not entirely compatible with threading.

You may run code and functionality in Python threads, but the App object itself needs an async event loop and I believe this should be done in the main program thread.

The reason for this design choice is that Jupyter notebooks are asynchronous and Jupylet was designed to fit in.

See here for more info:

https://docs.python.org/3/library/asyncio.html https://realpython.com/async-io-python/

Thanks for reporting!

cyberic99 commented 3 years ago

Hi, I've read the documentation you provided, thank you.

However, I still cannot run the app in a separate thread.

I tried to call asyncio.run_coroutine_threadsafe(app.run(), loop), but the render() function seems to be called only once.

Do I need to start the app somehow?

Here are the logs:

2021-06-23 18:28:02,888 - jupylet.event - INFO - Enter EventLeg.event(*args=(<function create_app.<locals>.key_event at 0x7f94687f70d0>,)).
2021-06-23 18:28:02,889 - jupylet.clock - INFO - Enter ClockLeg.schedule_interval(interval=0.041666666666666664, **kwargs={}).
2021-06-23 18:28:02,889 - jupylet.clock - INFO - Enter Scheduler.schedule_interval(foo=<function create_app.<locals>.modify_volume at 0x7f94687f71f0>, interval=0.041666666666666664, **kwargs={}).
2021-06-23 18:28:02,975 - jupylet.event - INFO - Enter EventLeg.event(*args=(<function create_app.<locals>.render at 0x7f94687f7310>,)).

Thanks

nir commented 3 years ago

It seems it should be possible to start an event loop in another thread. If you can do that, the app object should run in that "side" thread:

https://docs.python.org/3/library/asyncio-dev.html#concurrency-and-multithreading

cyberic99 commented 3 years ago

Thanks, I tried use asyncio in a thread, I can see the window but it stays black

import time
from jupylet.app import App
from threading import Thread
import logging
import asyncio
import sys

if __name__ == '__main__':

    def new_app():
        from jupylet.shadertoy import Shadertoy
        app = App(width=512, height=512, quality=100, mode="auto", log_level=logging.INFO)
        st = Shadertoy("""
            void mainImage( out vec4 fragColor, in vec2 fragCoord )
            {
                vec2 uv = fragCoord/iResolution.xy;
                vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
                fragColor = vec4(col,1.0);
            }
        """)

        @app.event
        def render(ct, dt):

            app.window.clear()
            st.draw(ct, dt)

        return app

    # in main thread, this works:
    # app = new_app()
    # app.run()

    # this doesn't work: the window is there but stays black
    def start_loop(loop):
        asyncio.set_event_loop(loop)
        loop.run_forever()
        print("exiting thread")

    app = new_app()
    app_loop = asyncio.new_event_loop()
    t = Thread(target=start_loop, args=(app_loop,), daemon=True)
    t.start()

    app_loop.call_soon(app.run)
    time.sleep(3)

    sys.exit(0)
cyberic99 commented 3 years ago

Just a small update, I have successfully run the app like this:

from jupylet.clock import setup_fake_time

app = new_app()
app.mode = "hidden"
app.fake_time=setup_fake_time()
app.start()
while True:
    time.sleep(0.1)
    app.step()

It is a bit hackish but it works and I can see he result on screen. I could also do that in hidden mode:

app = new_app()
app.start()
while True:
    time.sleep(0.1)
    app.step()
    print(app.observe())

unfortunately, I'd like to use another (Panda3D) OpenGL app at the same time, and I don't think it's possible

But at least this issue is solved now.

Thank you for your support.