|
8 | 8 | from . import platform |
9 | 9 | from .utils import get_device, read_texture, Lock |
10 | 10 | from .webgpu_api import * |
| 11 | +from functools import wraps |
11 | 12 |
|
12 | 13 | _TARGET_FPS = 60 |
13 | 14 |
|
14 | 15 |
|
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 |
67 | 83 |
|
68 | 84 | 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 | + |
74 | 188 |
|
75 | 189 |
|
76 | 190 | def init_webgpu(html_canvas): |
|
0 commit comments