vsjetengine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vsengine/__init__.py +28 -0
- vsengine/_futures.py +372 -0
- vsengine/_helpers.py +34 -0
- vsengine/_hospice.py +121 -0
- vsengine/_nodes.py +116 -0
- vsengine/_version.py +2 -0
- vsengine/adapters/__init__.py +6 -0
- vsengine/adapters/asyncio.py +85 -0
- vsengine/adapters/trio.py +107 -0
- vsengine/loops.py +269 -0
- vsengine/policy.py +395 -0
- vsengine/py.typed +0 -0
- vsengine/video.py +180 -0
- vsengine/vpy.py +441 -0
- vsjetengine-1.0.0.dist-info/METADATA +350 -0
- vsjetengine-1.0.0.dist-info/RECORD +18 -0
- vsjetengine-1.0.0.dist-info/WHEEL +4 -0
- vsjetengine-1.0.0.dist-info/licenses/COPYING +287 -0
vsengine/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
"""
|
|
7
|
+
vsengine - A common set of function that bridge vapoursynth with your application.
|
|
8
|
+
|
|
9
|
+
Parts:
|
|
10
|
+
- loops: Integrate vsengine with your event-loop (be it GUI-based or IO-based).
|
|
11
|
+
- policy: Create new isolated cores as needed.
|
|
12
|
+
- video: Get frames or render the video. Sans-IO and memory safe.
|
|
13
|
+
- vpy: Run .vpy-scripts in your application.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from vsengine.loops import *
|
|
17
|
+
from vsengine.policy import *
|
|
18
|
+
from vsengine.video import *
|
|
19
|
+
from vsengine.vpy import *
|
|
20
|
+
|
|
21
|
+
__version__: str
|
|
22
|
+
__version_tuple__: tuple[int | str, ...]
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from ._version import __version__, __version_tuple__
|
|
26
|
+
except ImportError:
|
|
27
|
+
__version__ = "0.0.0+unknown"
|
|
28
|
+
__version_tuple__ = (0, 0, 0, "+unknown")
|
vsengine/_futures.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator
|
|
9
|
+
from concurrent.futures import Future
|
|
10
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from inspect import isgeneratorfunction
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Any, Literal, Self, overload
|
|
15
|
+
|
|
16
|
+
from vsengine.loops import Cancelled, get_loop, keep_environment
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncContextManager[T, Any], Awaitable[T]):
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_call[**P](cls, func: Callable[P, Future[T]], *args: P.args, **kwargs: P.kwargs) -> Self:
|
|
22
|
+
try:
|
|
23
|
+
future = func(*args, **kwargs)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return cls.reject(e)
|
|
26
|
+
|
|
27
|
+
return cls.from_future(future)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_future(cls, future: Future[T]) -> Self:
|
|
31
|
+
if isinstance(future, cls):
|
|
32
|
+
return future
|
|
33
|
+
|
|
34
|
+
result = cls()
|
|
35
|
+
|
|
36
|
+
def _receive(fn: Future[T]) -> None:
|
|
37
|
+
if (exc := future.exception()) is not None:
|
|
38
|
+
result.set_exception(exc)
|
|
39
|
+
else:
|
|
40
|
+
result.set_result(future.result())
|
|
41
|
+
|
|
42
|
+
future.add_done_callback(_receive)
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def resolve(cls, value: T) -> Self:
|
|
47
|
+
future = cls()
|
|
48
|
+
future.set_result(value)
|
|
49
|
+
return future
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def reject(cls, error: BaseException) -> Self:
|
|
53
|
+
future = cls()
|
|
54
|
+
future.set_exception(error)
|
|
55
|
+
return future
|
|
56
|
+
|
|
57
|
+
# Adding callbacks
|
|
58
|
+
def add_done_callback(self, fn: Callable[[Future[T]], Any]) -> None:
|
|
59
|
+
# The done_callback should inherit the environment of the current call.
|
|
60
|
+
super().add_done_callback(keep_environment(fn))
|
|
61
|
+
|
|
62
|
+
def add_loop_callback(self, func: Callable[[Future[T]], None]) -> None:
|
|
63
|
+
def _wrapper(future: Future[T]) -> None:
|
|
64
|
+
get_loop().from_thread(func, future)
|
|
65
|
+
|
|
66
|
+
self.add_done_callback(_wrapper)
|
|
67
|
+
|
|
68
|
+
# Manipulating futures
|
|
69
|
+
@overload
|
|
70
|
+
def then[V](self, success_cb: Callable[[T], V], err_cb: None) -> UnifiedFuture[V]: ...
|
|
71
|
+
@overload
|
|
72
|
+
def then[V](self, success_cb: None, err_cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]: ...
|
|
73
|
+
def then[V](
|
|
74
|
+
self, success_cb: Callable[[T], V] | None, err_cb: Callable[[BaseException], V] | None
|
|
75
|
+
) -> UnifiedFuture[V] | UnifiedFuture[T | V]:
|
|
76
|
+
result = UnifiedFuture[T | V]()
|
|
77
|
+
|
|
78
|
+
def _run_cb(cb: Callable[[Any], V], v: Any) -> None:
|
|
79
|
+
try:
|
|
80
|
+
r = cb(v)
|
|
81
|
+
except BaseException as e:
|
|
82
|
+
result.set_exception(e)
|
|
83
|
+
else:
|
|
84
|
+
result.set_result(r)
|
|
85
|
+
|
|
86
|
+
def _done(fn: Future[T]) -> None:
|
|
87
|
+
if (exc := self.exception()) is not None:
|
|
88
|
+
if err_cb is not None:
|
|
89
|
+
_run_cb(err_cb, exc)
|
|
90
|
+
else:
|
|
91
|
+
result.set_exception(exc)
|
|
92
|
+
else:
|
|
93
|
+
if success_cb is not None:
|
|
94
|
+
_run_cb(success_cb, self.result())
|
|
95
|
+
else:
|
|
96
|
+
result.set_result(self.result())
|
|
97
|
+
|
|
98
|
+
self.add_done_callback(_done)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def map[V](self, cb: Callable[[T], V]) -> UnifiedFuture[V]:
|
|
102
|
+
return self.then(cb, None)
|
|
103
|
+
|
|
104
|
+
def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]:
|
|
105
|
+
return self.then(None, cb)
|
|
106
|
+
|
|
107
|
+
# Nicer Syntax
|
|
108
|
+
def __enter__(self) -> T:
|
|
109
|
+
obj = self.result()
|
|
110
|
+
|
|
111
|
+
if isinstance(obj, AbstractContextManager):
|
|
112
|
+
return obj.__enter__()
|
|
113
|
+
|
|
114
|
+
raise NotImplementedError("(async) with is not implemented for this object")
|
|
115
|
+
|
|
116
|
+
def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
|
|
117
|
+
obj = self.result()
|
|
118
|
+
|
|
119
|
+
if isinstance(obj, AbstractContextManager):
|
|
120
|
+
return obj.__exit__(exc, val, tb)
|
|
121
|
+
|
|
122
|
+
raise NotImplementedError("(async) with is not implemented for this object")
|
|
123
|
+
|
|
124
|
+
async def awaitable(self) -> T:
|
|
125
|
+
return await get_loop().await_future(self)
|
|
126
|
+
|
|
127
|
+
def __await__(self) -> Generator[Any, None, T]:
|
|
128
|
+
return self.awaitable().__await__()
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self) -> T:
|
|
131
|
+
result = await self.awaitable()
|
|
132
|
+
|
|
133
|
+
if isinstance(result, AbstractAsyncContextManager):
|
|
134
|
+
return await result.__aenter__()
|
|
135
|
+
if isinstance(result, AbstractContextManager):
|
|
136
|
+
return result.__enter__()
|
|
137
|
+
|
|
138
|
+
raise NotImplementedError("(async) with is not implemented for this object")
|
|
139
|
+
|
|
140
|
+
async def __aexit__(
|
|
141
|
+
self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None
|
|
142
|
+
) -> None:
|
|
143
|
+
result = await self.awaitable()
|
|
144
|
+
|
|
145
|
+
if isinstance(result, AbstractAsyncContextManager):
|
|
146
|
+
return await result.__aexit__(exc, val, tb)
|
|
147
|
+
if isinstance(result, AbstractContextManager):
|
|
148
|
+
return result.__exit__(exc, val, tb)
|
|
149
|
+
|
|
150
|
+
raise NotImplementedError("(async) with is not implemented for this object")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
|
|
154
|
+
def __init__(self, future_iterable: Iterator[Future[T]]) -> None:
|
|
155
|
+
self.future_iterable = future_iterable
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_call[**P](cls, func: Callable[P, Iterator[Future[T]]], *args: P.args, **kwargs: P.kwargs) -> Self:
|
|
159
|
+
return cls(func(*args, **kwargs))
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def futures(self) -> Iterator[Future[T]]:
|
|
163
|
+
return self.future_iterable
|
|
164
|
+
|
|
165
|
+
def run_as_completed(self, callback: Callable[[Future[T]], Any]) -> UnifiedFuture[None]:
|
|
166
|
+
state = UnifiedFuture[None]()
|
|
167
|
+
|
|
168
|
+
def _is_done_or_cancelled() -> bool:
|
|
169
|
+
if state.done():
|
|
170
|
+
return True
|
|
171
|
+
if state.cancelled():
|
|
172
|
+
state.set_exception(Cancelled())
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _get_next_future() -> Future[T] | None:
|
|
177
|
+
if _is_done_or_cancelled():
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
next_future = self.future_iterable.__next__()
|
|
182
|
+
except StopIteration:
|
|
183
|
+
state.set_result(None)
|
|
184
|
+
return None
|
|
185
|
+
except BaseException as e:
|
|
186
|
+
state.set_exception(e)
|
|
187
|
+
return None
|
|
188
|
+
return next_future
|
|
189
|
+
|
|
190
|
+
def _run_callbacks() -> None:
|
|
191
|
+
try:
|
|
192
|
+
while (future := _get_next_future()) is not None:
|
|
193
|
+
# Wait for the future to finish.
|
|
194
|
+
if not future.done():
|
|
195
|
+
future.add_done_callback(_continuation_in_foreign_thread)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Run the callback.
|
|
199
|
+
if not _run_single_callback(future):
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Try to give control back to the event loop.
|
|
203
|
+
next_cycle = get_loop().next_cycle()
|
|
204
|
+
if not next_cycle.done():
|
|
205
|
+
next_cycle.add_done_callback(_continuation_from_next_cycle)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# We do not have a real event loop here.
|
|
209
|
+
# If the next_cycle causes an error to bubble, forward it to the state future.
|
|
210
|
+
if next_cycle.exception() is not None:
|
|
211
|
+
state.set_exception(next_cycle.exception())
|
|
212
|
+
return
|
|
213
|
+
except Exception as e:
|
|
214
|
+
import traceback
|
|
215
|
+
|
|
216
|
+
traceback.print_exception(e)
|
|
217
|
+
state.set_exception(e)
|
|
218
|
+
|
|
219
|
+
def _continuation_from_next_cycle(fut: Future[None]) -> None:
|
|
220
|
+
if fut.exception() is not None:
|
|
221
|
+
state.set_exception(fut.exception())
|
|
222
|
+
else:
|
|
223
|
+
_run_callbacks()
|
|
224
|
+
|
|
225
|
+
def _continuation_in_foreign_thread(fut: Future[T]) -> None:
|
|
226
|
+
# Optimization, see below.
|
|
227
|
+
get_loop().from_thread(_continuation, fut)
|
|
228
|
+
|
|
229
|
+
def _continuation(fut: Future[T]) -> None:
|
|
230
|
+
if _run_single_callback(fut):
|
|
231
|
+
_run_callbacks()
|
|
232
|
+
|
|
233
|
+
@keep_environment
|
|
234
|
+
def _run_single_callback(fut: Future[T]) -> bool:
|
|
235
|
+
# True => Schedule next future.
|
|
236
|
+
# False => Cancel the loop.
|
|
237
|
+
if _is_done_or_cancelled():
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
result = callback(fut)
|
|
242
|
+
except BaseException as e:
|
|
243
|
+
state.set_exception(e)
|
|
244
|
+
return False
|
|
245
|
+
else:
|
|
246
|
+
if result is None or bool(result):
|
|
247
|
+
return True
|
|
248
|
+
else:
|
|
249
|
+
state.set_result(None)
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
# Optimization:
|
|
253
|
+
# We do not need to inherit any kind of environment as
|
|
254
|
+
# _run_single_callback will automatically set the environment for us.
|
|
255
|
+
get_loop().from_thread(_run_callbacks)
|
|
256
|
+
return state
|
|
257
|
+
|
|
258
|
+
def __iter__(self) -> Self:
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def __next__(self) -> T:
|
|
262
|
+
fut = self.future_iterable.__next__()
|
|
263
|
+
return fut.result()
|
|
264
|
+
|
|
265
|
+
def __aiter__(self) -> Self:
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
async def __anext__(self) -> T:
|
|
269
|
+
try:
|
|
270
|
+
fut = self.future_iterable.__next__()
|
|
271
|
+
except StopIteration:
|
|
272
|
+
raise StopAsyncIteration
|
|
273
|
+
return await get_loop().await_future(fut)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def unified[T, **P](
|
|
278
|
+
*,
|
|
279
|
+
kind: Literal["generator"],
|
|
280
|
+
) -> Callable[
|
|
281
|
+
[Callable[P, Iterator[Future[T]]]],
|
|
282
|
+
Callable[P, UnifiedIterator[T]],
|
|
283
|
+
]: ...
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@overload
|
|
287
|
+
def unified[T, **P](
|
|
288
|
+
*,
|
|
289
|
+
kind: Literal["future"],
|
|
290
|
+
) -> Callable[
|
|
291
|
+
[Callable[P, Future[T]]],
|
|
292
|
+
Callable[P, UnifiedFuture[T]],
|
|
293
|
+
]: ...
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@overload
|
|
297
|
+
def unified[T, **P](
|
|
298
|
+
*,
|
|
299
|
+
kind: Literal["generator"],
|
|
300
|
+
iterable_class: type[UnifiedIterator[T]],
|
|
301
|
+
) -> Callable[
|
|
302
|
+
[Callable[P, Iterator[Future[T]]]],
|
|
303
|
+
Callable[P, UnifiedIterator[T]],
|
|
304
|
+
]: ...
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@overload
|
|
308
|
+
def unified[T, **P](
|
|
309
|
+
*,
|
|
310
|
+
kind: Literal["future"],
|
|
311
|
+
future_class: type[UnifiedFuture[T]],
|
|
312
|
+
) -> Callable[
|
|
313
|
+
[Callable[P, Future[T]]],
|
|
314
|
+
Callable[P, UnifiedFuture[T]],
|
|
315
|
+
]: ...
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@overload
|
|
319
|
+
def unified[T, **P](
|
|
320
|
+
*,
|
|
321
|
+
kind: Literal["auto"] = "auto",
|
|
322
|
+
iterable_class: type[UnifiedIterator[Any]] = ...,
|
|
323
|
+
future_class: type[UnifiedFuture[Any]] = ...,
|
|
324
|
+
) -> Callable[
|
|
325
|
+
[Callable[P, Future[T] | Iterator[Future[T]]]],
|
|
326
|
+
Callable[P, UnifiedFuture[T] | UnifiedIterator[T]],
|
|
327
|
+
]: ...
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# Implementation
|
|
331
|
+
def unified[T, **P](
|
|
332
|
+
*,
|
|
333
|
+
kind: str = "auto",
|
|
334
|
+
iterable_class: type[UnifiedIterator[Any]] = UnifiedIterator[Any],
|
|
335
|
+
future_class: type[UnifiedFuture[Any]] = UnifiedFuture[Any],
|
|
336
|
+
) -> Any:
|
|
337
|
+
"""
|
|
338
|
+
Decorator to normalize functions returning Future[T] or Iterator[Future[T]]
|
|
339
|
+
into functions returning UnifiedFuture[T] or UnifiedIterator[T].
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
def _decorator_generator(func: Callable[P, Iterator[Future[T]]]) -> Callable[P, UnifiedIterator[T]]:
|
|
343
|
+
@wraps(func)
|
|
344
|
+
def _wrapped(*args: P.args, **kwargs: P.kwargs) -> UnifiedIterator[T]:
|
|
345
|
+
return iterable_class.from_call(func, *args, **kwargs)
|
|
346
|
+
|
|
347
|
+
return _wrapped
|
|
348
|
+
|
|
349
|
+
def _decorator_future(func: Callable[P, Future[T]]) -> Callable[P, UnifiedFuture[T]]:
|
|
350
|
+
@wraps(func)
|
|
351
|
+
def _wrapped(*args: P.args, **kwargs: P.kwargs) -> UnifiedFuture[T]:
|
|
352
|
+
return future_class.from_call(func, *args, **kwargs)
|
|
353
|
+
|
|
354
|
+
return _wrapped
|
|
355
|
+
|
|
356
|
+
def decorator(
|
|
357
|
+
func: Callable[P, Iterator[Future[T]]] | Callable[P, Future[T]],
|
|
358
|
+
) -> Callable[P, UnifiedIterator[T]] | Callable[P, UnifiedFuture[T]]:
|
|
359
|
+
if kind == "auto":
|
|
360
|
+
if isgeneratorfunction(func):
|
|
361
|
+
return _decorator_generator(func)
|
|
362
|
+
return _decorator_future(func) # type:ignore[arg-type]
|
|
363
|
+
|
|
364
|
+
if kind == "generator":
|
|
365
|
+
return _decorator_generator(func) # type:ignore[arg-type]
|
|
366
|
+
|
|
367
|
+
if kind == "future":
|
|
368
|
+
return _decorator_future(func) # type:ignore[arg-type]
|
|
369
|
+
|
|
370
|
+
raise NotImplementedError
|
|
371
|
+
|
|
372
|
+
return decorator
|
vsengine/_helpers.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
import contextlib
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
|
|
9
|
+
import vapoursynth as vs
|
|
10
|
+
|
|
11
|
+
from vsengine.policy import ManagedEnvironment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Automatically set the environment within that block.
|
|
15
|
+
@contextlib.contextmanager
|
|
16
|
+
def use_inline(function_name: str, env: vs.Environment | ManagedEnvironment | None) -> Iterator[None]:
|
|
17
|
+
if env is None:
|
|
18
|
+
# Ensure there is actually an environment set in this block.
|
|
19
|
+
try:
|
|
20
|
+
vs.get_current_environment()
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise OSError(
|
|
23
|
+
f"You are currently not running within an environment. "
|
|
24
|
+
f"Pass the environment directly to {function_name}."
|
|
25
|
+
) from e
|
|
26
|
+
yield
|
|
27
|
+
|
|
28
|
+
elif isinstance(env, ManagedEnvironment):
|
|
29
|
+
with env.inline_section():
|
|
30
|
+
yield
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
with env.use():
|
|
34
|
+
yield
|
vsengine/_hospice.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
import gc
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import weakref
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from vapoursynth import Core, EnvironmentData
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
lock = threading.Lock()
|
|
19
|
+
refctr = 0
|
|
20
|
+
refnanny = dict[int, weakref.ReferenceType[EnvironmentData]]()
|
|
21
|
+
cores = dict[int, Core]()
|
|
22
|
+
|
|
23
|
+
stage2_to_add = set[int]()
|
|
24
|
+
stage2 = set[int]()
|
|
25
|
+
stage1 = set[int]()
|
|
26
|
+
|
|
27
|
+
hold = set[int]()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def admit_environment(environment: EnvironmentData, core: Core) -> None:
|
|
31
|
+
global refctr
|
|
32
|
+
|
|
33
|
+
with lock:
|
|
34
|
+
ident = refctr
|
|
35
|
+
refctr += 1
|
|
36
|
+
|
|
37
|
+
ref = weakref.ref(environment, lambda _: _add_tostage1(ident))
|
|
38
|
+
cores[ident] = core
|
|
39
|
+
refnanny[ident] = ref
|
|
40
|
+
|
|
41
|
+
logger.debug("Admitted environment %r and %r as with ID:%s.", environment, core, ident)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def any_alive() -> bool:
|
|
45
|
+
if bool(stage1) or bool(stage2) or bool(stage2_to_add):
|
|
46
|
+
gc.collect()
|
|
47
|
+
if bool(stage1) or bool(stage2) or bool(stage2_to_add):
|
|
48
|
+
gc.collect()
|
|
49
|
+
if bool(stage1) or bool(stage2) or bool(stage2_to_add):
|
|
50
|
+
gc.collect()
|
|
51
|
+
return bool(stage1) or bool(stage2) or bool(stage2_to_add)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def freeze() -> None:
|
|
55
|
+
logger.debug("Freezing the hospice. Cores won't be collected anyore.")
|
|
56
|
+
|
|
57
|
+
hold.update(stage1)
|
|
58
|
+
hold.update(stage2)
|
|
59
|
+
hold.update(stage2_to_add)
|
|
60
|
+
stage1.clear()
|
|
61
|
+
stage2.clear()
|
|
62
|
+
stage2_to_add.clear()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def unfreeze() -> None:
|
|
66
|
+
stage1.update(hold)
|
|
67
|
+
hold.clear()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_core_still_used(ident: int) -> bool:
|
|
71
|
+
# There has to be the Core, CoreTimings and the temporary reference as an argument to getrefcount
|
|
72
|
+
# https://docs.python.org/3/library/sys.html#sys.getrefcount
|
|
73
|
+
return sys.getrefcount(cores[ident]) > 3
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _add_tostage1(ident: int) -> None:
|
|
77
|
+
logger.debug("Environment has died. Keeping core for a few gc-cycles. ID:%s", ident)
|
|
78
|
+
|
|
79
|
+
with lock:
|
|
80
|
+
stage1.add(ident)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _collectstage1(phase: Literal["start", "stop"], _: dict[str, int]) -> None:
|
|
84
|
+
if phase != "stop":
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
with lock:
|
|
88
|
+
for ident in tuple(stage1):
|
|
89
|
+
if _is_core_still_used(ident):
|
|
90
|
+
logger.warning("Core is still in use. ID:%s", ident)
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
stage1.remove(ident)
|
|
94
|
+
stage2_to_add.add(ident)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _collectstage2(phase: Literal["start", "stop"], _: dict[str, int]) -> None:
|
|
98
|
+
global stage2_to_add
|
|
99
|
+
|
|
100
|
+
if phase != "stop":
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
garbage = []
|
|
104
|
+
with lock:
|
|
105
|
+
for ident in tuple(stage2):
|
|
106
|
+
if _is_core_still_used(ident):
|
|
107
|
+
logger.warning("Core is still in use in stage 2. ID:%s", ident)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
stage2.remove(ident)
|
|
111
|
+
garbage.append(cores.pop(ident))
|
|
112
|
+
logger.debug("Marking core %r for collection", ident)
|
|
113
|
+
|
|
114
|
+
stage2.update(stage2_to_add)
|
|
115
|
+
stage2_to_add = set()
|
|
116
|
+
|
|
117
|
+
garbage.clear()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
gc.callbacks.append(_collectstage2)
|
|
121
|
+
gc.callbacks.append(_collectstage1)
|
vsengine/_nodes.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from concurrent.futures import Future
|
|
8
|
+
from threading import RLock
|
|
9
|
+
|
|
10
|
+
from vapoursynth import RawFrame, core
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def buffer_futures[FrameT: RawFrame](
|
|
14
|
+
futures: Iterable[Future[FrameT]], prefetch: int = 0, backlog: int | None = None
|
|
15
|
+
) -> Iterator[Future[FrameT]]:
|
|
16
|
+
if prefetch == 0:
|
|
17
|
+
prefetch = core.num_threads
|
|
18
|
+
if backlog is None:
|
|
19
|
+
backlog = prefetch * 3
|
|
20
|
+
if backlog < prefetch:
|
|
21
|
+
backlog = prefetch
|
|
22
|
+
|
|
23
|
+
enum_fut = enumerate(futures)
|
|
24
|
+
|
|
25
|
+
finished = False
|
|
26
|
+
running = 0
|
|
27
|
+
lock = RLock()
|
|
28
|
+
reorder = dict[int, Future[FrameT]]()
|
|
29
|
+
|
|
30
|
+
def _request_next() -> None:
|
|
31
|
+
nonlocal finished, running
|
|
32
|
+
with lock:
|
|
33
|
+
if finished:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
ni = next(enum_fut, None)
|
|
37
|
+
if ni is None:
|
|
38
|
+
finished = True
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
running += 1
|
|
42
|
+
|
|
43
|
+
idx, fut = ni
|
|
44
|
+
reorder[idx] = fut
|
|
45
|
+
fut.add_done_callback(_finished)
|
|
46
|
+
|
|
47
|
+
def _finished(f: Future[FrameT]) -> None:
|
|
48
|
+
nonlocal finished, running
|
|
49
|
+
with lock:
|
|
50
|
+
running -= 1
|
|
51
|
+
if finished:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if f.exception() is not None:
|
|
55
|
+
finished = True
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
_refill()
|
|
59
|
+
|
|
60
|
+
def _refill() -> None:
|
|
61
|
+
if finished:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
with lock:
|
|
65
|
+
# Two rules: 1. Don't exceed the concurrency barrier.
|
|
66
|
+
# 2. Don't exceed unused-frames-backlog
|
|
67
|
+
while (not finished) and (running < prefetch) and len(reorder) < backlog:
|
|
68
|
+
_request_next()
|
|
69
|
+
|
|
70
|
+
_refill()
|
|
71
|
+
|
|
72
|
+
sidx = 0
|
|
73
|
+
try:
|
|
74
|
+
while (not finished) or (len(reorder) > 0) or running > 0:
|
|
75
|
+
if sidx not in reorder:
|
|
76
|
+
# Spin. Reorder being empty should never happen.
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Get next requested frame
|
|
80
|
+
fut = reorder[sidx]
|
|
81
|
+
del reorder[sidx]
|
|
82
|
+
sidx += 1
|
|
83
|
+
_refill()
|
|
84
|
+
|
|
85
|
+
yield fut
|
|
86
|
+
|
|
87
|
+
finally:
|
|
88
|
+
finished = True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def close_when_needed[FrameT: RawFrame](future_iterable: Iterable[Future[FrameT]]) -> Iterator[Future[FrameT]]:
|
|
92
|
+
def copy_future_and_run_cb_before(fut: Future[FrameT]) -> Future[FrameT]:
|
|
93
|
+
f = Future[FrameT]()
|
|
94
|
+
|
|
95
|
+
def _as_completed(_: Future[FrameT]) -> None:
|
|
96
|
+
try:
|
|
97
|
+
r = fut.result()
|
|
98
|
+
except Exception as e:
|
|
99
|
+
f.set_exception(e)
|
|
100
|
+
else:
|
|
101
|
+
new_r = r.__enter__()
|
|
102
|
+
f.set_result(new_r)
|
|
103
|
+
|
|
104
|
+
fut.add_done_callback(_as_completed)
|
|
105
|
+
return f
|
|
106
|
+
|
|
107
|
+
def close_fut(f: Future[FrameT]) -> None:
|
|
108
|
+
def _do_close(_: Future[FrameT]) -> None:
|
|
109
|
+
if f.exception() is None:
|
|
110
|
+
f.result().__exit__(None, None, None)
|
|
111
|
+
|
|
112
|
+
f.add_done_callback(_do_close)
|
|
113
|
+
|
|
114
|
+
for fut in future_iterable:
|
|
115
|
+
yield copy_future_and_run_cb_before(fut)
|
|
116
|
+
close_fut(fut)
|
vsengine/_version.py
ADDED