changeset 2454:405abed9a45c default tip

fix up asyncio-in-bg-thread sorcery
author drewp@bigasterisk.com
date Mon, 19 May 2025 21:25:32 -0700
parents b23afde50bc2
children
files blender/__init__.py blender/asyncio_thread.py blender/time_sync/__init__.py blender/time_sync/time_from_graph.py src/light9/ascoltami/main.py
diffstat 5 files changed, 74 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/blender/__init__.py	Sun May 18 20:08:35 2025 -0700
+++ b/blender/__init__.py	Mon May 19 21:25:32 2025 -0700
@@ -26,6 +26,7 @@
             sys.path.append(p)
 
     for module in modules:
+        sys.stderr.write(f'🚋1 register {module}\n')
         module.register()
 
 
--- a/blender/asyncio_thread.py	Sun May 18 20:08:35 2025 -0700
+++ b/blender/asyncio_thread.py	Mon May 19 21:25:32 2025 -0700
@@ -1,16 +1,49 @@
 import asyncio
-from threading import Thread
+import sys
+from threading import Thread, get_ident
+import time
 from typing import Coroutine
 
 
+def log(msg):
+    try:
+        rl = hex(id(asyncio.get_running_loop()))
+    except RuntimeError:
+        rl = '(no loop ----)'
+    sys.stderr.write(f"thread={hex(get_ident())} loop={rl} {msg}\n")
+
+
 def startLoopInThread(task: Coroutine) -> asyncio.AbstractEventLoop:
+    """
+    run a new event loop in a background thread. `task` is run in the new loop.
+    Caller should use this (from fg thread) to run further tasks:
+      asyncio.run_coroutine_threadsafe(task, returned_loop)
 
-    def start_background_loop(loop: asyncio.AbstractEventLoop) -> None:
-        asyncio.set_event_loop(loop)
-        loop.run_forever()
+    """
+    log('🚋4 startLoopInThread enter ')
+    loops = []
+
+    def start_background_loop() -> None:
+
+        async def forever():
+            log('🚋6 log_loop')
+            loops.append(asyncio.get_running_loop())
+            while True:
+                await asyncio.sleep(100)
 
-    loop = asyncio.new_event_loop()
-    t = Thread(target=start_background_loop, args=(loop,), daemon=True)
+        log('🚋5 make asyncio loop')
+        asyncio.run(forever())
+        log('🚋19 start_background_loop done')
+
+    t = Thread(target=start_background_loop, daemon=True)
     t.start()
+    while not loops:
+        time.sleep(.1)
+    loop = loops[0]
+    log('🚋7 loop has started in thread')
+
     asyncio.run_coroutine_threadsafe(task, loop)
+    log('🚋8 started task')
+
+    log('🚋9 startLoopInThread exit')
     return loop
--- a/blender/time_sync/__init__.py	Sun May 18 20:08:35 2025 -0700
+++ b/blender/time_sync/__init__.py	Mon May 19 21:25:32 2025 -0700
@@ -3,12 +3,15 @@
 blender-side time changes are sent back to ascoltami
 """
 
+import sys
+
 sync: object = None
 
 
 def register():
     global sync
     from .time_from_graph import Sync
+    sys.stderr.write('🚋2 imported Sync\n')
     sync = Sync()
 
 
--- a/blender/time_sync/time_from_graph.py	Sun May 18 20:08:35 2025 -0700
+++ b/blender/time_sync/time_from_graph.py	Mon May 19 21:25:32 2025 -0700
@@ -1,13 +1,18 @@
+import asyncio
 import threading
 import time
+from typing import Coroutine, cast
 
 import bpy  # type: ignore
 from bpy.app.handlers import persistent  # type: ignore
-from light9_sync.asyncio_thread import startLoopInThread
+from ..asyncio_thread import startLoopInThread
 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
+from rdflib import URIRef
+from rdflib.graph import _ContextType
 
-from light9 import networking
+from light9 import networking, showconfig
 from light9.namespaces import L9
+from light9.newtypes import decimalLiteral
 from light9.run_local import log
 from light9.typedgraph import typedValue
 
@@ -31,16 +36,26 @@
         # main thread
         self.lastSetFrame = -1
         self.lastGraphFrame = -1
-        startLoopInThread(self.task())
+        show = showconfig.showUri()
+        self.ctx = cast(_ContextType, (URIRef(show + '/ascoltami')))
+        log.debug('🚋3 startLoopInThread')
+        self._loop = startLoopInThread(self.task())
+
         # need persistent because `blender --addons ...` seems to start addon
         # before loading scene.
         bpy.app.timers.register(self.update, persistent=True)
         bpy.app.handlers.frame_change_post.append(self.on_frame_change_post)
+        log.info('🚋10 Sync initd')
 
     ## updates from graph -> self
 
+    def runInBackgroundLoop(self, f: Coroutine):
+        asyncio.run_coroutine_threadsafe(f, self._loop)
+
+
     async def task(self):
         # bg thread with asyncio loop
+        log.info('🚋11 start SyncedGraph')
         self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
         self.graph.addHandler(self.syncFromGraph)
 
@@ -63,7 +78,7 @@
 
     ## graph time -> blender time
 
-    # @persistent
+    @persistent
     def update(self):
         # main thread? wherever blender runs timers
         if self.playing:
@@ -90,7 +105,15 @@
             # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration)
             self.lastSetFrame = scene.frame_current
             t = scene.frame_current / scene.render.fps
-            self.setInGraph(t)
+            self.runInBackgroundLoop(self.setInGraph(t, bpy.context.screen.is_animation_playing))
 
-    def setInGraph(self, t: float):
-        log.warning(f'todo: set graph to {t:.2f}')
+    async def setInGraph(self, t: float, isBlenderPlaying: bool):
+        log.info(f'set graph time to {t:.2f}')
+        if isBlenderPlaying:
+            log.info('  playing mode')
+            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(t))
+        else:
+            log.info('  paused mode')
+            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(t))
+
+        await self.graph.patch(p)
--- a/src/light9/ascoltami/main.py	Sun May 18 20:08:35 2025 -0700
+++ b/src/light9/ascoltami/main.py	Mon May 19 21:25:32 2025 -0700
@@ -42,6 +42,7 @@
                 g.add((asc, L9['song'], self.playlist.songUri(s.song), self.ctx))
             except KeyError:
                 pass
+        # maybe share the rest of this with time_from_graph.py
         g.add((asc, L9['duration'], decimalLiteral(s.duration), self.ctx))
         g.add((asc, L9['playing'], Literal(s.playing), self.ctx))
         if s.wallStartTime is not None: