pyglet 2.1.dev5__py3-none-any.whl → 2.1.dev6__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.
- pyglet/__init__.py +33 -14
- pyglet/__init__.pyi +15 -1
- pyglet/app/__init__.py +1 -1
- pyglet/app/base.py +1 -1
- pyglet/app/cocoa.py +4 -0
- pyglet/display/__init__.py +5 -1
- pyglet/display/__init__.pyi +4 -0
- pyglet/event.py +11 -15
- pyglet/experimental/geoshader_sprite.py +12 -9
- pyglet/experimental/jobs.py +124 -0
- pyglet/experimental/multitexture_sprite.py +20 -11
- pyglet/experimental/particles.py +11 -9
- pyglet/gl/__init__.py +5 -3
- pyglet/graphics/__init__.py +57 -19
- pyglet/graphics/shader.py +18 -15
- pyglet/graphics/vertexbuffer.py +80 -36
- pyglet/graphics/vertexdomain.py +99 -144
- pyglet/gui/widgets.py +31 -18
- pyglet/image/__init__.py +570 -611
- pyglet/image/animation.py +7 -8
- pyglet/image/codecs/png.py +2 -2
- pyglet/input/__init__.py +6 -6
- pyglet/input/base.py +2 -0
- pyglet/input/linux/evdev.py +29 -39
- pyglet/math.py +103 -72
- pyglet/media/codecs/base.py +3 -109
- pyglet/media/drivers/base.py +88 -4
- pyglet/media/player.py +2 -9
- pyglet/model/__init__.py +34 -41
- pyglet/model/codecs/base.py +104 -0
- pyglet/model/codecs/gltf.py +295 -172
- pyglet/model/codecs/obj.py +33 -39
- pyglet/resource.py +6 -9
- pyglet/shapes.py +8 -5
- pyglet/sprite.py +23 -4
- pyglet/text/__init__.py +1 -1
- pyglet/window/__init__.py +87 -49
- pyglet/window/cocoa/__init__.py +35 -13
- pyglet/window/cocoa/pyglet_delegate.py +36 -8
- pyglet/window/cocoa/pyglet_view.py +13 -4
- pyglet/window/headless/__init__.py +1 -1
- pyglet/window/win32/__init__.py +50 -28
- pyglet/window/xlib/__init__.py +26 -16
- {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/METADATA +1 -1
- {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/RECORD +47 -44
- {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/LICENSE +0 -0
- {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/WHEEL +0 -0
pyglet/__init__.py
CHANGED
|
@@ -8,14 +8,14 @@ import os
|
|
|
8
8
|
import sys
|
|
9
9
|
from collections.abc import ItemsView, Sequence
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from types import FrameType
|
|
15
15
|
from typing import Any, Callable, ItemsView, Sized
|
|
16
16
|
|
|
17
17
|
#: The release version
|
|
18
|
-
version = '2.1.
|
|
18
|
+
version = '2.1.dev6'
|
|
19
19
|
__version__ = version
|
|
20
20
|
|
|
21
21
|
MIN_PYTHON_VERSION = 3, 8
|
|
@@ -48,6 +48,7 @@ _OPTION_TYPE_VALIDATORS = {
|
|
|
48
48
|
"int": lambda x: isinstance(x, int),
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
|
|
51
52
|
@dataclass
|
|
52
53
|
class Options:
|
|
53
54
|
"""Dataclass for global pyglet options."""
|
|
@@ -249,16 +250,29 @@ class Options:
|
|
|
249
250
|
|
|
250
251
|
.. versionadded:: 2.0.5"""
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
"""For 'HiDPI'
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
253
|
+
dpi_scaling: Literal["real", "scaled", "stretch"] = "real"
|
|
254
|
+
"""For 'HiDPI' displays, Window behavior can differ between operating systems. Defaults to `'real'`.
|
|
255
|
+
|
|
256
|
+
The current options are an attempt to create consistent behavior across all of the operating systems.
|
|
257
|
+
|
|
258
|
+
`'real'` (default): Provides a 1:1 pixel for Window frame size and framebuffer. Primarily used for game applications
|
|
259
|
+
to ensure you are getting the exact pixels for the resolution. If you provide an 800x600 window, you can ensure it
|
|
260
|
+
will be 800x600 pixels when the user chooses it.
|
|
261
|
+
|
|
262
|
+
`'scaled'`: Window size is scaled based on the DPI ratio. Window size and content (projection) size matches the full
|
|
263
|
+
framebuffer. Primarily used for any applications that wish to become DPI aware. You must rescale and reposition your
|
|
264
|
+
content to take advantage of the larger framebuffer. An 800x600 with a 150% DPI scaling would be changed to
|
|
265
|
+
1200x900 for both `window.get_size` and `window.get_framebuffer_size()`.
|
|
266
|
+
|
|
267
|
+
Keep in mind that pyglet objects may not be scaled proportionately, so this is left up to the developer.
|
|
268
|
+
The :py:attr:`~pyglet.window.Window.scale` & :py:attr:`~pyglet.window.Window.dpi` attributes can be queried as a
|
|
269
|
+
reference when determining object creation.
|
|
270
|
+
|
|
271
|
+
`'stretch'`: Window is scaled based on the DPI ratio. However, content size matches original requested size of the
|
|
272
|
+
window, and is stretched to fit the full framebuffer. This mimics behavior of having no DPI scaling at all. No
|
|
273
|
+
rescaling and repositioning of content will be necessary, but at the cost of blurry content depending on the extent
|
|
274
|
+
of the stretch. For example, 800x600 at 150% DPI will be 800x600 for `window.get_size()` and 1200x900 for
|
|
275
|
+
`window.get_framebuffer_size()`.
|
|
262
276
|
"""
|
|
263
277
|
|
|
264
278
|
shader_bind_management: bool = True
|
|
@@ -282,7 +296,7 @@ class Options:
|
|
|
282
296
|
|
|
283
297
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
284
298
|
assert key in self.__annotations__, f"Invalid option name: '{key}'"
|
|
285
|
-
assert (_SPECIAL_OPTION_VALIDATORS.get(key
|
|
299
|
+
assert (_SPECIAL_OPTION_VALIDATORS.get(key) or _OPTION_TYPE_VALIDATORS[self.__annotations__[key]])(value), \
|
|
286
300
|
f"Invalid type: '{type(value)}' for '{key}'"
|
|
287
301
|
self.__dict__[key] = value
|
|
288
302
|
|
|
@@ -290,11 +304,16 @@ class Options:
|
|
|
290
304
|
#: Instance of :py:class:`~pyglet.Options` used to set runtime options.
|
|
291
305
|
options: Options = Options()
|
|
292
306
|
|
|
307
|
+
_OPTION_TYPE_REMAPS = {
|
|
308
|
+
"audio": "sequence",
|
|
309
|
+
"vsync": "bool",
|
|
310
|
+
}
|
|
293
311
|
|
|
294
312
|
for _key, _type in options.__annotations__.items():
|
|
295
313
|
"""Check Environment Variables for pyglet options"""
|
|
296
314
|
if _value := os.environ.get(f"PYGLET_{_key.upper()}"):
|
|
297
|
-
|
|
315
|
+
_type = _OPTION_TYPE_REMAPS.get(_key, _type)
|
|
316
|
+
if _type == 'sequence':
|
|
298
317
|
options[_key] = _value.split(",")
|
|
299
318
|
elif _type == 'bool':
|
|
300
319
|
options[_key] = _value in ("true", "TRUE", "True", "1")
|
pyglet/__init__.pyi
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Sequence
|
|
3
|
+
from typing import Any, ItemsView, Literal, Sequence
|
|
4
4
|
|
|
5
5
|
from . import app as app
|
|
6
6
|
from . import clock as clock
|
|
@@ -62,6 +62,20 @@ class Options:
|
|
|
62
62
|
win32_disable_xinput: bool
|
|
63
63
|
com_mta: bool
|
|
64
64
|
osx_alt_loop: bool
|
|
65
|
+
dpi_scaling: Literal["real", "scaled", "stretch"]
|
|
66
|
+
scale_with_dpi: bool
|
|
65
67
|
shader_bind_management: bool
|
|
66
68
|
|
|
69
|
+
def get(self, item: str, default: Any = None) -> Any:
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def items(self) -> ItemsView[str, Any]:
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def __getitem__(self, item: str) -> Any:
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def __setitem__(self, key: str, item: Any) -> None:
|
|
79
|
+
...
|
|
80
|
+
|
|
67
81
|
options: Options
|
pyglet/app/__init__.py
CHANGED
|
@@ -68,7 +68,7 @@ the set when they are no longer referenced or are closed explicitly.
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def run(interval:
|
|
71
|
+
def run(interval: float | None = 1/60) -> None:
|
|
72
72
|
"""Begin processing events, scheduled functions and window updates.
|
|
73
73
|
|
|
74
74
|
This is a convenience function, equivalent to::
|
pyglet/app/base.py
CHANGED
|
@@ -112,7 +112,7 @@ class EventLoop(event.EventDispatcher):
|
|
|
112
112
|
for window in app.windows:
|
|
113
113
|
window.draw(dt)
|
|
114
114
|
|
|
115
|
-
def run(self, interval:
|
|
115
|
+
def run(self, interval: float | None = 1/60) -> None:
|
|
116
116
|
"""Begin processing events, scheduled functions and window updates.
|
|
117
117
|
|
|
118
118
|
This method enters into the main event loop and, if the ``interval``
|
pyglet/app/cocoa.py
CHANGED
|
@@ -71,6 +71,10 @@ class _AppDelegate_Implementation:
|
|
|
71
71
|
def applicationDidFinishLaunching_(self, notification):
|
|
72
72
|
self._pyglet_loop._finished_launching = True
|
|
73
73
|
|
|
74
|
+
@_AppDelegate.method('B')
|
|
75
|
+
def applicationSupportsSecureRestorableState_(self):
|
|
76
|
+
return True
|
|
77
|
+
|
|
74
78
|
_AppDelegate = ObjCClass('_AppDelegate') # the actual class
|
|
75
79
|
|
|
76
80
|
class CocoaAlternateEventLoop(EventLoop):
|
pyglet/display/__init__.py
CHANGED
|
@@ -44,10 +44,13 @@ else:
|
|
|
44
44
|
from pyglet.display.win32 import Win32Display as Display
|
|
45
45
|
from pyglet.display.win32 import Win32Screen as Screen
|
|
46
46
|
from pyglet.display.win32 import Win32Canvas as Canvas
|
|
47
|
-
|
|
47
|
+
elif compat_platform == 'linux':
|
|
48
48
|
from pyglet.display.xlib import XlibDisplay as Display
|
|
49
49
|
from pyglet.display.xlib import XlibScreen as Screen
|
|
50
50
|
from pyglet.display.xlib import XlibCanvas as Canvas
|
|
51
|
+
else:
|
|
52
|
+
msg = f"A display interface for '{compat_platform}' is not yet implemented."
|
|
53
|
+
raise NotImplementedError(msg)
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
_displays: weakref.WeakSet = weakref.WeakSet()
|
|
@@ -73,4 +76,5 @@ def get_display() -> Display:
|
|
|
73
76
|
# Otherwise, create a new display and return it.
|
|
74
77
|
return Display()
|
|
75
78
|
|
|
79
|
+
|
|
76
80
|
__all__ = ['Display', 'Screen', 'Canvas', 'ScreenMode', 'get_display']
|
pyglet/event.py
CHANGED
|
@@ -120,23 +120,19 @@ from __future__ import annotations
|
|
|
120
120
|
|
|
121
121
|
import inspect
|
|
122
122
|
import os.path
|
|
123
|
-
|
|
124
123
|
from functools import partial
|
|
125
|
-
from typing import TYPE_CHECKING
|
|
124
|
+
from typing import TYPE_CHECKING, Literal, Union
|
|
126
125
|
from weakref import WeakMethod
|
|
127
126
|
|
|
128
127
|
if TYPE_CHECKING:
|
|
129
|
-
import sys
|
|
130
128
|
from typing import Any, Callable, Generator
|
|
131
|
-
if sys.version_info >= (3, 11):
|
|
132
|
-
from typing import Self
|
|
133
|
-
else:
|
|
134
|
-
Self = Any
|
|
135
129
|
|
|
136
130
|
|
|
137
131
|
EVENT_HANDLED = True
|
|
138
132
|
EVENT_UNHANDLED = None
|
|
139
133
|
|
|
134
|
+
EVENT_HANDLE_STATE = Union[Literal[True], None]
|
|
135
|
+
|
|
140
136
|
|
|
141
137
|
class EventException(Exception): # noqa: N818
|
|
142
138
|
"""An exception raised when an event handler could not be attached."""
|
|
@@ -152,7 +148,7 @@ class EventDispatcher:
|
|
|
152
148
|
_event_stack: tuple | list = ()
|
|
153
149
|
|
|
154
150
|
@classmethod
|
|
155
|
-
def register_event_type(cls: type[
|
|
151
|
+
def register_event_type(cls: type[object], name: str) -> str:
|
|
156
152
|
"""Register an event type with the dispatcher.
|
|
157
153
|
|
|
158
154
|
Before dispatching events, they must first be registered by name.
|
|
@@ -161,8 +157,8 @@ class EventDispatcher:
|
|
|
161
157
|
for suitable handlers.
|
|
162
158
|
"""
|
|
163
159
|
if not hasattr(cls, 'event_types'):
|
|
164
|
-
cls.event_types = []
|
|
165
|
-
cls.event_types.append(name)
|
|
160
|
+
cls.event_types = [] # type: ignore reportAttributeAccessIssue
|
|
161
|
+
cls.event_types.append(name) # type: ignore reportAttributeAccessIssue
|
|
166
162
|
return name
|
|
167
163
|
|
|
168
164
|
def push_handlers(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -181,15 +177,15 @@ class EventDispatcher:
|
|
|
181
177
|
self._event_stack = []
|
|
182
178
|
|
|
183
179
|
# Place dict full of new handlers at beginning of stack
|
|
184
|
-
self._event_stack.insert(0, {})
|
|
180
|
+
self._event_stack.insert(0, {}) # type: ignore reportAttributeAccessIssue
|
|
185
181
|
self.set_handlers(*args, **kwargs)
|
|
186
182
|
|
|
187
|
-
def _get_handlers(self, args: list, kwargs: dict) -> Generator[str, Callable]:
|
|
183
|
+
def _get_handlers(self, args: list, kwargs: dict) -> Generator[tuple[str, Callable], None, None]:
|
|
188
184
|
"""Implement handler matching on arguments for set_handlers and remove_handlers."""
|
|
189
185
|
for obj in args:
|
|
190
186
|
if inspect.isroutine(obj):
|
|
191
187
|
# Single magically named function
|
|
192
|
-
name = obj.__name__
|
|
188
|
+
name: str = obj.__name__
|
|
193
189
|
if name not in self.event_types:
|
|
194
190
|
msg = f'Unknown event "{name}"'
|
|
195
191
|
raise EventException(msg)
|
|
@@ -438,10 +434,10 @@ class EventDispatcher:
|
|
|
438
434
|
def _dump_handlers(self) -> None:
|
|
439
435
|
|
|
440
436
|
for level, handlers in enumerate(self._event_stack):
|
|
441
|
-
print(f"level: {level}")
|
|
437
|
+
print(f"level: {level}")
|
|
442
438
|
|
|
443
439
|
for event_type, handler in handlers.items():
|
|
444
|
-
print(f" - '{event_type}': {handler}")
|
|
440
|
+
print(f" - '{event_type}': {handler}")
|
|
445
441
|
|
|
446
442
|
# Decorator
|
|
447
443
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sys
|
|
2
4
|
|
|
3
5
|
import pyglet
|
|
4
|
-
|
|
6
|
+
from pyglet import clock, event, graphics, image
|
|
5
7
|
from pyglet.gl import *
|
|
6
|
-
from pyglet import clock
|
|
7
|
-
from pyglet import event
|
|
8
|
-
from pyglet import graphics
|
|
9
|
-
from pyglet import image
|
|
10
8
|
|
|
11
9
|
_is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
|
|
12
10
|
|
|
@@ -209,7 +207,7 @@ class SpriteGroup(graphics.Group):
|
|
|
209
207
|
self.program.stop()
|
|
210
208
|
|
|
211
209
|
def __repr__(self):
|
|
212
|
-
return "{
|
|
210
|
+
return f"{self.__class__.__name__}({self.texture})"
|
|
213
211
|
|
|
214
212
|
def __eq__(self, other):
|
|
215
213
|
return (other.__class__ is self.__class__ and
|
|
@@ -328,8 +326,14 @@ class Sprite(event.EventDispatcher):
|
|
|
328
326
|
self._group.blend_dest,
|
|
329
327
|
program,
|
|
330
328
|
self._user_group)
|
|
331
|
-
self._batch
|
|
332
|
-
|
|
329
|
+
if (self._batch and
|
|
330
|
+
self._batch.update_shader(self._vertex_list, GL_POINTS, self._group, program)):
|
|
331
|
+
# Exit early if changing domain is not needed.
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# Recreate vertex list.
|
|
335
|
+
self._vertex_list.delete()
|
|
336
|
+
self._create_vertex_list()
|
|
333
337
|
|
|
334
338
|
def delete(self):
|
|
335
339
|
"""Force immediate removal of the sprite from video memory.
|
|
@@ -596,7 +600,6 @@ class Sprite(event.EventDispatcher):
|
|
|
596
600
|
`scale_y` : float
|
|
597
601
|
Vertical scaling factor.
|
|
598
602
|
"""
|
|
599
|
-
|
|
600
603
|
translations_outdated = False
|
|
601
604
|
|
|
602
605
|
# only bother updating if the translation actually changed
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Experimental multithreaded Job system.
|
|
2
|
+
|
|
3
|
+
This module provides the `JobExecutor` class, which allows submitting
|
|
4
|
+
batches of highly parallizable work in the form of functions and arguments.
|
|
5
|
+
This is commonly known as "task-based multithreading" or a "multithreaded
|
|
6
|
+
job system".
|
|
7
|
+
|
|
8
|
+
..note:: This module is only really useful with the recent Python 3.13
|
|
9
|
+
releases that are built with experimental free-threading support.
|
|
10
|
+
With typical Python releases that contain a GIL, this is not of
|
|
11
|
+
much practical use.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from queue import Queue
|
|
19
|
+
from threading import Event, Thread
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from typing import Callable, Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Worker(Thread):
|
|
27
|
+
"""A worker thread to pop and run jobs."""
|
|
28
|
+
def __init__(self, workqueue: Queue, exitevent: Event, name: str) -> None:
|
|
29
|
+
super().__init__(name=name, daemon=True)
|
|
30
|
+
self.queue = workqueue
|
|
31
|
+
self.exit = exitevent
|
|
32
|
+
self.start()
|
|
33
|
+
|
|
34
|
+
def run(self) -> None:
|
|
35
|
+
"""Parallel thread of execution."""
|
|
36
|
+
_exit = self.exit
|
|
37
|
+
_queue = self.queue
|
|
38
|
+
while not _exit.is_set():
|
|
39
|
+
func, args = _queue.get()
|
|
40
|
+
func(*args)
|
|
41
|
+
_queue.task_done()
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"{self.name}(id={self.native_id})"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class JobExecutor:
|
|
48
|
+
"""A light-weight multithreaded job system.
|
|
49
|
+
|
|
50
|
+
A JobExecutor loosely mimics the design of the Executor classes from the
|
|
51
|
+
`concurrent.futures` module, but does not share any code with those classes.
|
|
52
|
+
Instead, it is a more light-weight implementation intended for execution of
|
|
53
|
+
highly parallizable functions (jobs). A key difference is that JobExecutor
|
|
54
|
+
does not return `Futures`, and instead depends on the submitted jobs to
|
|
55
|
+
self-contained.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, max_workers: int | None = None) -> None:
|
|
59
|
+
"""Create an instance of a JobExecutor.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
max_workers: The number of threads to use. If `None`, will
|
|
63
|
+
create half as many threads as reported CPU cores.
|
|
64
|
+
"""
|
|
65
|
+
self._max_workers = max_workers or os.cpu_count() // 2
|
|
66
|
+
self._exitevent = Event()
|
|
67
|
+
self._queue = Queue()
|
|
68
|
+
self._threads = [_Worker(self._queue, self._exitevent, f"Thread{i+1}") for i in range(self._max_workers)]
|
|
69
|
+
|
|
70
|
+
def submit(self, func: Callable, *args: Any) -> None:
|
|
71
|
+
"""Submit a job to be executed on .
|
|
72
|
+
|
|
73
|
+
A "job" consists of a function to be called, alone with any arguments
|
|
74
|
+
that should be passed to it. Jobs are automatically executed by the next
|
|
75
|
+
free worker thread. No values are returned when jobs are submitted,
|
|
76
|
+
and the functions should also not return any values; return values are
|
|
77
|
+
lost.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
func: A function to execute.
|
|
81
|
+
*args: Any arguments to pass to the function.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
self._queue.put((func, args))
|
|
85
|
+
except AttributeError:
|
|
86
|
+
raise RuntimeError("cannot submit new tasks after shutdown")
|
|
87
|
+
|
|
88
|
+
def shutdown(self) -> None:
|
|
89
|
+
"""Shut down the JobExecutor, terminating all worker threads.
|
|
90
|
+
|
|
91
|
+
All JobExecutor workers are Daemon Threads, so it is not strictly
|
|
92
|
+
necessary to call shutdown() at program termination. However, if
|
|
93
|
+
it is no longer needed, shutdown() can be called ot free up the
|
|
94
|
+
thread resources.
|
|
95
|
+
"""
|
|
96
|
+
if not self._queue:
|
|
97
|
+
return
|
|
98
|
+
self._exitevent.set()
|
|
99
|
+
for _ in range(self._max_workers):
|
|
100
|
+
self.submit(lambda: None)
|
|
101
|
+
self._threads.clear()
|
|
102
|
+
self._exitevent = None
|
|
103
|
+
self._queue = None
|
|
104
|
+
|
|
105
|
+
def sync(self) -> None:
|
|
106
|
+
"""Wait for all currently submitted jobs to complete.
|
|
107
|
+
|
|
108
|
+
This method will wait until the internal queue is empty, AND all
|
|
109
|
+
currently submitted jobs have completed execution. It can be used
|
|
110
|
+
to fence between separate batches of jobs that should not be run
|
|
111
|
+
at the same time. For example::
|
|
112
|
+
|
|
113
|
+
for chunk in worklist:
|
|
114
|
+
executor.submit(some_function, chunk)
|
|
115
|
+
|
|
116
|
+
executor.sync()
|
|
117
|
+
|
|
118
|
+
window.draw()
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
self._queue.join()
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f"{self.__class__.__name__}(workers={self._threads})"
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
"""Allows for a somewhat generic multitextured sprite.
|
|
2
2
|
|
|
3
3
|
This sprite behaves just like regular sprites.
|
|
4
|
-
|
|
5
4
|
"""
|
|
6
5
|
from __future__ import annotations
|
|
7
6
|
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
8
9
|
import pyglet
|
|
9
|
-
from pyglet.gl import glActiveTexture,
|
|
10
|
-
|
|
10
|
+
from pyglet.gl import glActiveTexture, glBindTexture, glEnable, GL_BLEND, glBlendFunc, glDisable, glGetIntegerv, GLint
|
|
11
|
+
from pyglet.gl import GL_SRC_ALPHA, GL_TEXTURE0, GL_ONE_MINUS_SRC_ALPHA, GL_TRIANGLES, GL_MAX_TEXTURE_IMAGE_UNITS
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pyglet.image import Texture, AbstractImage, Animation
|
|
15
|
+
from pyglet.graphics import Batch, Group, ShaderProgram
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
class MultiTextureSpriteGroup(pyglet.graphics.Group):
|
|
13
19
|
"""Shared Multi-texture Sprite rendering Group.
|
|
@@ -104,6 +110,7 @@ class MultiTextureSpriteGroup(pyglet.graphics.Group):
|
|
|
104
110
|
tuple([texture.id for texture in self._textures.values()]) +
|
|
105
111
|
tuple([texture.target for texture in self._textures.values()]))
|
|
106
112
|
|
|
113
|
+
|
|
107
114
|
# Allows the default shader to pick the appropriate sampler for the fragment shader
|
|
108
115
|
_SAMPLER_TYPES = {
|
|
109
116
|
pyglet.gl.GL_TEXTURE_2D: "sampler2D",
|
|
@@ -116,6 +123,7 @@ _SAMPLER_COORDS = {
|
|
|
116
123
|
pyglet.gl.GL_TEXTURE_2D_ARRAY: ""
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
|
|
119
127
|
def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
|
|
120
128
|
"""Creates the default multi-texture shader based on the dict of textures passed in.
|
|
121
129
|
|
|
@@ -126,7 +134,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
|
|
|
126
134
|
"""
|
|
127
135
|
max_tex = GLint()
|
|
128
136
|
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, max_tex)
|
|
129
|
-
assert len(images) <= max_tex.value, f"
|
|
137
|
+
assert len(images) <= max_tex.value, f"Only {max_tex.value} Texture Units are available."
|
|
130
138
|
|
|
131
139
|
# Generate the default vertex shader
|
|
132
140
|
in_tex_coords = '\n'.join([f"in vec3 {name}_coords;" for name in images.keys()])
|
|
@@ -176,7 +184,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
|
|
|
176
184
|
|
|
177
185
|
in_tex_coords = '\n'.join([f"in vec3 {name}_coords_frag;" for name in images.keys()])
|
|
178
186
|
uniform_samplers = '\n'.join([f"uniform {_SAMPLER_TYPES[tex.target]} {name};" for name,tex in images.items()])
|
|
179
|
-
tex_operations = '\n'.join([f" color = layer(texture({name}, {name}_coords_frag{_SAMPLER_COORDS[tex.target]}), color);" for name,tex in images.items()])
|
|
187
|
+
tex_operations = '\n'.join([f" color = layer(texture({name}, {name}_coords_frag{_SAMPLER_COORDS[tex.target]}), color);" for name, tex in images.items()])
|
|
180
188
|
fragment_source = f"""
|
|
181
189
|
#version 150 core
|
|
182
190
|
|
|
@@ -200,6 +208,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
|
|
|
200
208
|
|
|
201
209
|
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'), (fragment_source, 'fragment'))
|
|
202
210
|
|
|
211
|
+
|
|
203
212
|
class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
204
213
|
"""Creates a multi-textured sprite.
|
|
205
214
|
|
|
@@ -259,7 +268,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
259
268
|
Args:
|
|
260
269
|
images:
|
|
261
270
|
A dict object with the key being the name of the texture and the
|
|
262
|
-
value is either an Animation or AbstractImage.
|
|
271
|
+
value is either an Animation or AbstractImage. Currently,
|
|
263
272
|
each item must be of the same size.
|
|
264
273
|
x:
|
|
265
274
|
X coordinate of the sprite.
|
|
@@ -280,7 +289,6 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
280
289
|
subpixel:
|
|
281
290
|
Allow floating-point coordinates for the sprite. By default,
|
|
282
291
|
coordinates are restricted to integer values.
|
|
283
|
-
|
|
284
292
|
program:
|
|
285
293
|
A specific shader program to initialize the sprite with. The
|
|
286
294
|
default multi-texture overlay shader will be used if one is
|
|
@@ -392,7 +400,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
392
400
|
self._vertex_list = self._program.vertex_list_indexed(
|
|
393
401
|
4, GL_TRIANGLES, [0, 1, 2, 0, 2, 3], self._batch, self._group,
|
|
394
402
|
position=('f', self._get_vertices()),
|
|
395
|
-
colors=('Bn',
|
|
403
|
+
colors=('Bn', self._rgba * 4),
|
|
396
404
|
translate=('f', (self._x, self._y, self._z) * 4),
|
|
397
405
|
scale=('f', (self._scale * self._scale_x, self._scale * self._scale_y) * 4),
|
|
398
406
|
rotation=('f', (self._rotation,) * 4),
|
|
@@ -455,6 +463,8 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
455
463
|
Args:
|
|
456
464
|
name:
|
|
457
465
|
The dict key given for the texture layer in the constructor
|
|
466
|
+
img:
|
|
467
|
+
The Image or Animation to set
|
|
458
468
|
"""
|
|
459
469
|
if name in self._animations:
|
|
460
470
|
# Need to stop all animations temporarly so we can swap the layer out
|
|
@@ -465,7 +475,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
465
475
|
tex = None
|
|
466
476
|
if isinstance(img, pyglet.image.Animation):
|
|
467
477
|
# Add the animation and schedule it based on pause
|
|
468
|
-
self._animations[name] = {
|
|
478
|
+
self._animations[name] = {"animation": img, "frame_idx": 0, "next_dt": img.frames[0].duration}
|
|
469
479
|
if img.frames[0].duration and not self._paused:
|
|
470
480
|
pyglet.clock.schedule_once(self._animate, self._animations[name]["next_dt"], name)
|
|
471
481
|
|
|
@@ -481,7 +491,6 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
481
491
|
# Only update if we actually changed the "base" texture
|
|
482
492
|
self._update_position()
|
|
483
493
|
|
|
484
|
-
|
|
485
494
|
@property
|
|
486
495
|
def frame_index(self) -> None:
|
|
487
496
|
raise NotImplementedError("MultiTextureSprite does not support the frame_index property. Use get_frame_index instead.")
|
|
@@ -512,7 +521,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
|
|
|
512
521
|
if pause:
|
|
513
522
|
pyglet.clock.unschedule(self._animate)
|
|
514
523
|
else:
|
|
515
|
-
# Kick off all
|
|
524
|
+
# Kick off all animations again
|
|
516
525
|
for name, animation in self._animations.items():
|
|
517
526
|
frame = animation["animation"].frames[animation["frame_idx"]]
|
|
518
527
|
if frame.duration:
|
pyglet/experimental/particles.py
CHANGED
|
@@ -6,12 +6,8 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
8
|
import pyglet
|
|
9
|
-
|
|
9
|
+
from pyglet import clock, event, graphics, image
|
|
10
10
|
from pyglet.gl import *
|
|
11
|
-
from pyglet import clock
|
|
12
|
-
from pyglet import event
|
|
13
|
-
from pyglet import graphics
|
|
14
|
-
from pyglet import image
|
|
15
11
|
|
|
16
12
|
_is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
|
|
17
13
|
|
|
@@ -299,8 +295,14 @@ class Emitter(event.EventDispatcher):
|
|
|
299
295
|
self._group.blend_dest,
|
|
300
296
|
program,
|
|
301
297
|
self._user_group)
|
|
302
|
-
self._batch
|
|
303
|
-
|
|
298
|
+
if (self._batch and
|
|
299
|
+
self._batch.update_shader(self._vertex_list, GL_POINTS, self._group, program)):
|
|
300
|
+
# Exit early if changing domain is not needed.
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Recreate vertex list.
|
|
304
|
+
self._vertex_list.delete()
|
|
305
|
+
self._create_vertex_list()
|
|
304
306
|
|
|
305
307
|
def delete(self):
|
|
306
308
|
"""Force immediate removal of the emitter from video memory.
|
|
@@ -407,7 +409,7 @@ class ParticleManager:
|
|
|
407
409
|
|
|
408
410
|
# TODO: remove debug
|
|
409
411
|
self.total_number -= 1
|
|
410
|
-
self.total_label.text = f"particles: {
|
|
412
|
+
self.total_label.text = f"particles: {self.total_number * self._count * 8!s}"
|
|
411
413
|
|
|
412
414
|
def create_emitter(self, x, y, z=0):
|
|
413
415
|
emitter = Emitter(self._img, x, y, z, self._count, self._velocity, self._spread,
|
|
@@ -418,6 +420,6 @@ class ParticleManager:
|
|
|
418
420
|
|
|
419
421
|
# TODO: remove debug
|
|
420
422
|
self.total_number += 1
|
|
421
|
-
self.total_label.text = f"particles: {
|
|
423
|
+
self.total_label.text = f"particles: {self.total_number * self._count * 8!s}"
|
|
422
424
|
|
|
423
425
|
return emitter
|
pyglet/gl/__init__.py
CHANGED
|
@@ -162,16 +162,18 @@ def _create_shadow_window() -> None:
|
|
|
162
162
|
from pyglet.window import Window
|
|
163
163
|
|
|
164
164
|
class ShadowWindow(Window):
|
|
165
|
+
_shadow = True
|
|
165
166
|
def __init__(self) -> None:
|
|
166
167
|
super().__init__(width=1, height=1, visible=False)
|
|
167
168
|
|
|
168
169
|
def _create_projection(self) -> None:
|
|
169
170
|
"""Shadow window does not need a projection."""
|
|
170
|
-
pass
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def _on_internal_resize(self, width: int, height: int) -> None:
|
|
173
|
+
"""No projection and not required."""
|
|
174
|
+
|
|
175
|
+
def _on_internal_scale(self, scale: float, dpi: int) -> None:
|
|
173
176
|
"""No projection and not required."""
|
|
174
|
-
pass
|
|
175
177
|
|
|
176
178
|
_shadow_window = ShadowWindow()
|
|
177
179
|
_shadow_window.switch_to()
|