Skip to content

Commit 5e089bf

Browse files
committed
another debounce attempt
1 parent 3b8f991 commit 5e089bf

1 file changed

Lines changed: 171 additions & 57 deletions

File tree

webgpu/canvas.py

Lines changed: 171 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,69 +8,183 @@
88
from . import platform
99
from .utils import get_device, read_texture, Lock
1010
from .webgpu_api import *
11+
from functools import wraps
1112

1213
_TARGET_FPS = 60
1314

1415

15-
@dataclass
16-
class _DebounceData:
17-
t_last_frame: float = 0
18-
t_last_call: float = 0
19-
timer: threading.Timer | None = None
20-
lock: Lock = None
21-
22-
23-
def debounce(arg=None):
24-
def decorator(func):
25-
# Render only once every 1/_TARGET_FPS seconds
26-
@functools.wraps(func)
27-
def debounced(obj, *args, **kwargs):
28-
if not hasattr(obj, "_debounce_data"):
29-
obj._debounce_data = {}
30-
31-
fname = func.__name__
32-
if obj._debounce_data.get(fname, None) is None:
33-
obj._debounce_data[fname] = _DebounceData(0, 0, None, Lock())
34-
35-
data = obj._debounce_data[fname]
36-
t_call = time.time()
37-
data.t_last_call = t_call
38-
39-
frame_time = 1.0 / target_fps
40-
41-
def f():
42-
with data.lock:
43-
if t_call != data.t_last_call and t_call - data.t_last_frame < frame_time:
44-
return
45-
46-
data.t_last_frame = time.time()
47-
func(obj, *args, **kwargs)
48-
49-
t_wait = frame_time - (t_call - data.t_last_frame)
50-
51-
if t_wait <= 0:
52-
f()
53-
return
54-
55-
if platform.is_pyodide:
56-
import asyncio
57-
async def _runner():
58-
if t_wait > 0:
59-
await asyncio.sleep(t_wait)
60-
f()
61-
asyncio.create_task(_runner())
62-
else:
63-
threading.Timer(t_wait, f).start()
64-
65-
debounced._original = func
66-
return debounced
16+
# @dataclass
17+
# class _DebounceData:
18+
# t_last_frame: float = 0
19+
# t_last_call: float = 0
20+
# timer: threading.Timer | None = None
21+
# lock: Lock = None
22+
# running: bool = False
23+
# pending: bool = False
24+
25+
def debounce(arg=None, *, rate_hz=60):
26+
27+
def _rate_limited(fn, rate_hz):
28+
interval = 1.0 / rate_hz
29+
lock = threading.RLock()
30+
last_call = 0.0
31+
timer = None
32+
pending = None
33+
34+
def schedule(delay):
35+
nonlocal timer
36+
timer = threading.Timer(delay, run_pending)
37+
timer.daemon = True
38+
timer.start()
39+
40+
def run_pending():
41+
nonlocal last_call, timer, pending
42+
43+
with lock:
44+
if pending is None:
45+
timer = None
46+
return
47+
48+
args, kwargs = pending
49+
pending = None
50+
# print("call frequency = ", 1.0 / (time.monotonic() - last_call))
51+
last_call = time.monotonic()
52+
53+
fn(*args, **kwargs)
54+
55+
with lock:
56+
timer = None
57+
if pending is not None:
58+
delay = max(0.0, interval - (time.monotonic() - last_call))
59+
schedule(delay)
60+
61+
@wraps(fn)
62+
def wrapper(*args, **kwargs):
63+
nonlocal last_call, pending
64+
65+
with lock:
66+
now = time.monotonic()
67+
elapsed = now - last_call
68+
if elapsed >= interval and timer is None:
69+
# print("call frequency = ", 1.0 / elapsed if elapsed > 0 else float('inf'))
70+
last_call = now
71+
run_now = True
72+
else:
73+
pending = (args, kwargs)
74+
run_now = False
75+
76+
if timer is None:
77+
schedule(max(0.0, interval - elapsed))
78+
79+
if run_now:
80+
fn(*args, **kwargs)
81+
82+
return wrapper
6783

6884
if callable(arg):
69-
target_fps = _TARGET_FPS
70-
return decorator(arg)
71-
else:
72-
target_fps = arg
73-
return decorator
85+
return _rate_limited(arg, rate_hz)
86+
87+
if arg is not None:
88+
rate_hz = arg
89+
90+
def decorate(fn):
91+
return _rate_limited(fn, rate_hz)
92+
return decorate
93+
94+
95+
96+
# def debounce(arg=None):
97+
# def decorator(func):
98+
# # Render only once every 1/_TARGET_FPS seconds
99+
# @functools.wraps(func)
100+
# def debounced(obj, *args, **kwargs):
101+
# if not hasattr(obj, "_debounce_data"):
102+
# obj._debounce_data = {}
103+
104+
# fname = func.__name__
105+
# if obj._debounce_data.get(fname, None) is None:
106+
# obj._debounce_data[fname] = _DebounceData(0, 0, None, Lock())
107+
108+
# data = obj._debounce_data[fname]
109+
# frame_time = 1.0 / target_fps
110+
111+
# def run():
112+
# while True:
113+
# # Call func OUTSIDE the lock to avoid deadlocks
114+
# func(obj, *args, **kwargs)
115+
116+
# with data.lock:
117+
# if not data.pending:
118+
# data.running = False
119+
# return
120+
# data.pending = False
121+
# elapsed = time.time() - data.t_last_frame
122+
# t_wait = frame_time - elapsed
123+
# if t_wait > 0:
124+
# # Schedule deferred re-run to respect frame rate
125+
# if platform.is_pyodide:
126+
# import asyncio
127+
# async def _rerun():
128+
# await asyncio.sleep(t_wait)
129+
# with data.lock:
130+
# data.t_last_frame = time.time()
131+
# run()
132+
# asyncio.create_task(_rerun())
133+
# else:
134+
# def _deferred():
135+
# with data.lock:
136+
# data.t_last_frame = time.time()
137+
# run()
138+
# data.timer = threading.Timer(t_wait, _deferred)
139+
# data.timer.start()
140+
# return
141+
# data.t_last_frame = time.time()
142+
143+
# def f():
144+
# with data.lock:
145+
# if t_call != data.t_last_call and t_call - data.t_last_frame < frame_time:
146+
# return
147+
# if data.running:
148+
# data.pending = True
149+
# return
150+
# data.running = True
151+
# data.t_last_frame = time.time()
152+
153+
# run()
154+
155+
# with data.lock:
156+
# t_call = time.time()
157+
# data.t_last_call = t_call
158+
# t_wait = frame_time - (t_call - data.t_last_frame)
159+
160+
# if t_wait <= 0:
161+
# if data.running:
162+
# data.pending = True
163+
# return
164+
# data.running = True
165+
# data.t_last_frame = time.time()
166+
# if data.timer is not None:
167+
# data.timer.cancel()
168+
# data.timer = None
169+
# else:
170+
# if data.timer is not None:
171+
# data.timer.cancel()
172+
# if platform.is_pyodide:
173+
# import asyncio
174+
# async def _runner():
175+
# await asyncio.sleep(t_wait)
176+
# f()
177+
# asyncio.create_task(_runner())
178+
# else:
179+
# data.timer = threading.Timer(t_wait, f)
180+
# data.timer.start()
181+
# return
182+
183+
# run()
184+
185+
# debounced._original = func
186+
# return debounced
187+
74188

75189

76190
def init_webgpu(html_canvas):

0 commit comments

Comments
 (0)