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 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
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.0"
2
+ __version_tuple__ = (1, 0, 0)
@@ -0,0 +1,6 @@
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
+