pyglet 2.1.12__py3-none-any.whl → 3.0.dev1__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.
Files changed (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +4 -17
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +27 -5
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +147 -177
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.12.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {pyglet-2.1.12.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,600 @@
1
+ """Low-level graphics rendering and abstractions.
2
+
3
+ This module provides efficient abstractions over OpenGL objects, such as
4
+ Shaders and Buffers. It also provides classes for highly performant batched
5
+ rendering and grouping.
6
+
7
+ See the :ref:`guide_graphics` for details on how to use this graphics API.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Callable, Sequence, Any, TYPE_CHECKING
13
+
14
+ import pyglet
15
+ from pyglet.graphics.api.gl import OpenGLSurfaceContext
16
+ from pyglet.graphics.api.gl.enums import geometry_map
17
+ from pyglet.graphics.api.gl2.vertexdomain import VertexList, IndexedVertexList, VertexDomain, IndexedVertexDomain, \
18
+ InstancedVertexDomain, InstancedIndexedVertexDomain
19
+ from pyglet.graphics.draw import _DomainKey, BatchBase, Group
20
+
21
+
22
+ _debug_graphics_batch = pyglet.options.debug_graphics_batch
23
+
24
+ if TYPE_CHECKING:
25
+ from pyglet.graphics.state import State
26
+ from pyglet.graphics import GeometryMode
27
+ from pyglet.graphics.api.gl2.shader import ShaderProgram
28
+
29
+
30
+
31
+
32
+ # Default Shader source (Primitives):
33
+
34
+ _vertex_source: str = """#version 110
35
+ attribute vec3 position;
36
+ attribute vec4 colors;
37
+
38
+ varying vec4 vertex_colors;
39
+
40
+ uniform mat4 u_projection;
41
+ uniform mat4 u_view;
42
+
43
+ void main()
44
+ {
45
+ gl_Position = u_projection * u_view * vec4(position, 1.0);
46
+
47
+ vertex_colors = colors;
48
+ }
49
+ """
50
+
51
+ _fragment_source: str = """#version 110
52
+ varying vec4 vertex_colors;
53
+
54
+ void main()
55
+ {
56
+ gl_FragColor = vertex_colors;
57
+ }
58
+ """
59
+
60
+
61
+ def get_default_batch() -> Batch:
62
+ """Batch used globally for objects that have no Batch specified."""
63
+ return pyglet.graphics.api.core.get_default_batch()
64
+ # try:
65
+ # return pyglet.graphics.api.core.current_context.pyglet_graphics_default_batch
66
+ # except AttributeError:
67
+ # pyglet.graphics.api.core.current_context.pyglet_graphics_default_batch = Batch()
68
+ # return pyglet.graphics.api.core.current_context.pyglet_graphics_default_batch
69
+
70
+
71
+ def get_default_shader() -> ShaderProgram:
72
+ """A default basic shader for default batches."""
73
+ return pyglet.graphics.api.core.get_cached_shader(
74
+ "default_graphics",
75
+ (_vertex_source, 'vertex'),
76
+ (_fragment_source, 'fragment'),
77
+ )
78
+
79
+
80
+ _domain_class_map: dict[tuple[bool, bool], type[VertexDomain]] = {
81
+ # Indexed, Instanced : Domain
82
+ (False, False): VertexDomain,
83
+ (True, False): IndexedVertexDomain,
84
+ (False, True): InstancedVertexDomain,
85
+ (True, True): InstancedIndexedVertexDomain,
86
+ }
87
+
88
+
89
+
90
+
91
+ class Batch(BatchBase):
92
+ """Manage a collection of drawables for batched rendering.
93
+
94
+ Many drawable pyglet objects accept an optional `Batch` argument in their
95
+ constructors. By giving a `Batch` to multiple objects, you can tell pyglet
96
+ that you expect to draw all of these objects at once, so it can optimise its
97
+ use of OpenGL. Hence, drawing a `Batch` is often much faster than drawing
98
+ each contained drawable separately.
99
+
100
+ The following example creates a batch, adds two sprites to the batch, and
101
+ then draws the entire batch::
102
+
103
+ batch = pyglet.graphics.Batch()
104
+ car = pyglet.sprite.Sprite(car_image, batch=batch)
105
+ boat = pyglet.sprite.Sprite(boat_image, batch=batch)
106
+
107
+ def on_draw():
108
+ batch.draw()
109
+
110
+ While any drawables can be added to a `Batch`, only those with the same
111
+ draw mode, shader program, and group can be optimised together.
112
+
113
+ Internally, a `Batch` manages a set of VertexDomains along with
114
+ information about how the domains are to be drawn. To implement batching on
115
+ a custom drawable, get your vertex domains from the given batch instead of
116
+ setting them up yourself.
117
+ """
118
+ _draw_list: list[Callable]
119
+ top_groups: list[Group]
120
+ group_children: dict[Group, list[Group]]
121
+ group_map: dict[Group, dict[_DomainKey, vertexdomain.VertexDomain]]
122
+
123
+ def __init__(self, context: OpenGLSurfaceContext | None = None, initial_count: int = 32) -> None:
124
+ """Initialize the batch for use.
125
+
126
+ Args:
127
+ context:
128
+ The OpenGL Surface context this batch will be a part of.
129
+ initial_count:
130
+ The initial element count of the buffers created by the domains in the batch.
131
+
132
+ Increase this value if you plan to load large amounts of vertices, as it will reduce the need for
133
+ resizing buffers, which can be slow.
134
+ """
135
+ # Mapping to find domain.
136
+ # group -> (attributes, mode, indexed) -> domain
137
+ super().__init__(initial_count)
138
+ self._context = context or pyglet.graphics.api.core.current_context
139
+ assert self._context is not None, "A context needs to exist before you create this."
140
+
141
+ def invalidate(self) -> None:
142
+ """Force the batch to update the draw list.
143
+
144
+ This method can be used to force the batch to re-compute the draw list
145
+ when the ordering of groups has changed.
146
+
147
+ .. versionadded:: 1.2
148
+ """
149
+ self._draw_list_dirty = True
150
+
151
+ def update_shader(self, vertex_list: VertexList | IndexedVertexList, mode: GeometryMode, group: Group,
152
+ program: ShaderProgram) -> bool:
153
+ """Migrate a vertex list to another domain that has the specified shader attributes.
154
+
155
+ The results are undefined if `mode` is not correct or if `vertex_list`
156
+ does not belong to this batch (they are not checked and will not
157
+ necessarily throw an exception immediately).
158
+
159
+ Args:
160
+ vertex_list:
161
+ A vertex list currently belonging to this batch.
162
+ mode:
163
+ The current GL drawing mode of the vertex list.
164
+ group:
165
+ The new group to migrate to.
166
+ program:
167
+ The new shader program to migrate to.
168
+
169
+ Returns:
170
+ False if the domain's no longer match. The caller should handle this scenario.
171
+ """
172
+ # No new attributes.
173
+ attributes = program.attributes.copy()
174
+
175
+ # Formats may differ (normalization) than what is declared in the shader.
176
+ # Make those adjustments and attempt to get a domain.
177
+ for a_name in attributes:
178
+ if (a_name in vertex_list.initial_attribs and
179
+ vertex_list.initial_attribs[a_name]['format'] != attributes[a_name]['format']):
180
+ attributes[a_name]['format'] = vertex_list.initial_attribs[a_name]['format']
181
+
182
+ domain = self.get_domain(vertex_list.indexed, vertex_list.instanced, mode, group, attributes)
183
+
184
+ # TODO: Allow migration if we can restore original vertices somehow. Much faster.
185
+ # If the domain's don't match, we need to re-create the vertex list. Tell caller no match.
186
+ if domain != vertex_list.domain:
187
+ return False
188
+
189
+ return True
190
+
191
+ def migrate(self, vertex_list: VertexList | IndexedVertexList, mode: GeometryMode, group: Group,
192
+ batch: Batch) -> None:
193
+ """Migrate a vertex list to another batch and/or group.
194
+
195
+ `vertex_list` and `mode` together identify the vertex list to migrate.
196
+ `group` and `batch` are new owners of the vertex list after migration.
197
+
198
+ The results are undefined if `mode` is not correct or if `vertex_list`
199
+ does not belong to this batch (they are not checked and will not
200
+ necessarily throw an exception immediately).
201
+
202
+ ``batch`` can remain unchanged if only a group change is desired.
203
+
204
+ Args:
205
+ vertex_list:
206
+ A vertex list currently belonging to this batch.
207
+ mode:
208
+ The current GL drawing mode of the vertex list.
209
+ group:
210
+ The new group to migrate to.
211
+ batch:
212
+ The batch to migrate to (or the current batch).
213
+
214
+ """
215
+ attributes = vertex_list.domain.attribute_meta
216
+ domain = batch.get_domain(vertex_list.indexed, vertex_list.instanced, mode, group, attributes)
217
+
218
+ if domain != vertex_list.domain:
219
+ vertex_list.migrate(domain, group)
220
+ else:
221
+ # If the same domain, no need to move vertices, just update the group.
222
+ vertex_list.update_group(group)
223
+
224
+ # Updating group can potentially change draw order though.
225
+ self._draw_list_dirty = True
226
+
227
+ def get_domain(self, indexed: bool, instanced: bool, mode: GeometryMode, group: Group,
228
+ attributes: dict[str, Any]) -> VertexDomain | IndexedVertexDomain | InstancedVertexDomain | InstancedIndexedVertexDomain:
229
+ """Get, or create, the vertex domain corresponding to the given arguments.
230
+
231
+ mode is the render mode such as GL_LINES or GL_TRIANGLES
232
+ """
233
+ # Group map just used for group lookup now, not domains.
234
+ if group not in self.group_map:
235
+ self._add_group(group)
236
+
237
+ # If instanced, ensure a separate domain, as multiple instance sources can match the key.
238
+ # Find domain given formats, indices and mode
239
+ key = _DomainKey(indexed, instanced, mode, str(attributes))
240
+
241
+ try:
242
+ domain = self._domain_registry[key]
243
+ except KeyError:
244
+ # Create domain
245
+ domain = _domain_class_map[(indexed, instanced)](self._context, self.initial_count, attributes)
246
+ self._domain_registry[key] = domain
247
+ self._draw_list_dirty = True
248
+ return domain
249
+
250
+ def _add_group(self, group: Group) -> None:
251
+ self.group_map[group] = {}
252
+ if group.parent is None:
253
+ self.top_groups.append(group)
254
+ else:
255
+ if group.parent not in self.group_map:
256
+ self._add_group(group.parent)
257
+ if group.parent not in self.group_children:
258
+ self.group_children[group.parent] = []
259
+ self.group_children[group.parent].append(group)
260
+
261
+ group._assigned_batches.add(self) # noqa: SLF001
262
+ self._draw_list_dirty = True
263
+
264
+ def _cleanup_groups(self, group: Group) -> None:
265
+ """Safely remove empty groups from all tracking structures."""
266
+ del self.group_map[group]
267
+ group._assigned_batches.remove(self) # noqa: SLF001
268
+ if group.parent:
269
+ self.group_children[group.parent].remove(group)
270
+ try: # noqa: SIM105
271
+ del self.group_children[group]
272
+ except KeyError:
273
+ pass
274
+ try: # noqa: SIM105
275
+ self.top_groups.remove(group)
276
+ except ValueError:
277
+ pass
278
+ # Remove bucket from all domains.
279
+ for domain in self._domain_registry.values():
280
+ if domain.has_bucket(group):
281
+ del domain._vertex_buckets[group] # noqa: SLF001
282
+
283
+ def _create_draw_list(self) -> list[Callable]:
284
+ """Rebuild draw list by walking the group tree and minimizing state transitions."""
285
+ def visit(group: Group) -> list[Callable]:
286
+ draw_list = []
287
+ if not group.visible:
288
+ return draw_list
289
+
290
+ # Determine if any vertices are drawable.
291
+ is_drawable = False
292
+ for dkey, domain in self._domain_registry.items():
293
+ if domain.is_empty or not domain.get_drawable_bucket(group):
294
+ self._empty_domains.add(dkey)
295
+ continue
296
+ is_drawable = True
297
+ break
298
+
299
+ # State has nothing drawable and no children to affect.
300
+ # Even if it has states, nothing to apply it to. Clean them and return early.
301
+ if not is_drawable and not self.group_children.get(group):
302
+ self._cleanup_groups(group)
303
+ return []
304
+
305
+ # If drawable, then find the domains to draw.
306
+ if is_drawable:
307
+ for dkey, domain in self._domain_registry.items():
308
+ bucket = domain.get_drawable_bucket(group)
309
+ if not bucket:
310
+ continue
311
+
312
+ draw_list.append((domain, dkey.mode, group))
313
+
314
+ # Recurse into visible children
315
+ children = self.group_children.get(group, [])
316
+ for child in sorted(children):
317
+ if child.visible:
318
+ draw_list.extend(visit(child))
319
+
320
+ if children or is_drawable:
321
+ return [(None, 'set', group), *draw_list, (None, 'unset', group)]
322
+
323
+ return draw_list
324
+
325
+ _draw_list = []
326
+
327
+ self.top_groups.sort()
328
+
329
+ for top in list(self.top_groups):
330
+ if top.visible:
331
+ _draw_list.extend(visit(top))
332
+
333
+ return _draw_list
334
+
335
+ @staticmethod
336
+ def _vao_bind_fn(domain): # noqa: ANN001, ANN205
337
+ def _bind_vao(_ctx) -> None: # noqa: ANN001
338
+ domain.bind_vao()
339
+
340
+ return _bind_vao
341
+
342
+ @staticmethod
343
+ def _draw_bucket_fn(domain, buckets, mode_func): # noqa: ANN001, ANN205
344
+ def _draw(_ctx) -> None: # noqa: ANN001
345
+ domain.draw_buckets(mode_func, buckets)
346
+
347
+ return _draw
348
+
349
+ def _optimize_draw_list(self, draw_list: list[tuple]) -> list[Callable]:
350
+ """Turn a flattened (domain/mode/group) list into optimized callables.
351
+
352
+ States that are equal (by __eq__) are treated as the same logical GPU state.
353
+ Intermediate unset/set pairs for identical states are removed, preserving
354
+ dependency ordering and teardown safety.
355
+ """
356
+ calls: list[Callable] = []
357
+ active_states: dict[type, State] = {} # (type, key) -> instance
358
+
359
+ def _next_same_type_set(idx: int, state_type: type) -> None | State:
360
+ for j in range(idx + 1, len(draw_list)):
361
+ dom2, mode2, group2 = draw_list[j]
362
+ if dom2 is None and mode2 == "set":
363
+ for s2 in group2._expanded_states: # noqa: SLF001
364
+ if type(s2) is state_type:
365
+ return s2 # first same-type set we see in the future
366
+ return None
367
+
368
+ def flush_buckets() -> None:
369
+ nonlocal current_buckets, last_mode, last_domain
370
+ if not current_buckets:
371
+ return
372
+
373
+ calls.append(self._draw_bucket_fn(last_domain, list(current_buckets), geometry_map[last_mode]))
374
+ current_buckets.clear()
375
+
376
+ def _emit_set(_state: State) -> None:
377
+ stype = type(_state)
378
+ current = active_states.get(stype)
379
+
380
+ # state is same
381
+ if current == _state:
382
+ return
383
+
384
+ # state changed
385
+ if current_buckets:
386
+ flush_buckets()
387
+
388
+ # unset previous
389
+ if current and current.unsets_state:
390
+ calls.append(current.unset_state)
391
+
392
+ # set new state
393
+ if _state.sets_state:
394
+ calls.append(_state.set_state)
395
+
396
+ active_states[stype] = _state
397
+
398
+ def _emit_unset(_state: State, idx: int) -> None:
399
+ stype = type(_state)
400
+ current = active_states.get(stype)
401
+ if current is None:
402
+ return
403
+
404
+ next_set = _next_same_type_set(idx, stype)
405
+ if next_set == current:
406
+ return # will remain active
407
+
408
+ # state is about to end
409
+ flush_buckets()
410
+
411
+ if current.unsets_state:
412
+ calls.append(current.unset_state)
413
+ active_states.pop(stype, None)
414
+
415
+ last_domain = None
416
+ last_mode = None
417
+ current_buckets = []
418
+
419
+ for i, (domain, mode, group) in enumerate(draw_list):
420
+ if domain is None:
421
+ # This is a state boundary. See if it *actually* changes state; if so, flush first.
422
+ if mode == "set":
423
+ for s in group._expanded_states:
424
+ _emit_set(s)
425
+
426
+ elif mode == "unset":
427
+ for s in reversed(group._expanded_states):
428
+ _emit_unset(s, i)
429
+
430
+ continue
431
+
432
+ # Drawable
433
+ bucket = domain.get_drawable_bucket(group)
434
+ if not bucket or bucket.is_empty:
435
+ continue
436
+
437
+ if last_domain is None:
438
+ calls.append(self._vao_bind_fn(domain))
439
+ elif domain != last_domain:
440
+ # New VAO: flush previous batch, bind new VAO
441
+ flush_buckets()
442
+ calls.append(self._vao_bind_fn(domain))
443
+ elif mode != last_mode:
444
+ # Same VAO but different primitive mode: flush
445
+ flush_buckets()
446
+
447
+ current_buckets.append(bucket)
448
+ last_domain = domain
449
+ last_mode = mode
450
+
451
+ # Final flush
452
+ flush_buckets()
453
+
454
+ return calls
455
+
456
+ def _dump_draw_list_call_order(self, draw_list: list[Callable], include_dc: bool=True) -> None:
457
+ import inspect
458
+
459
+ print("=== DRAW ORDER ===")
460
+
461
+ def fn_label(_fn: Callable) -> str:
462
+ r = repr(_fn)
463
+ if "unset_state" in r:
464
+ return "UNSET"
465
+ if "set_state" in r:
466
+ return "SET"
467
+ if "_vao_call_fn" in r:
468
+ return "VAO"
469
+ if "make_draw_buckets_fn" in r:
470
+ return "DRAW"
471
+ return "CALL"
472
+
473
+ def fn_name(_fn: Callable) -> str:
474
+ if inspect.ismethod(_fn):
475
+ dc_info = f" ({_fn.__self__})" if include_dc else ""
476
+ return f"{_fn.__self__.__class__.__name__}.{_fn.__name__}{dc_info}"
477
+ if inspect.isfunction(_fn):
478
+ return _fn.__name__
479
+ return _fn.__class__.__name__
480
+
481
+ for i, fn in enumerate(draw_list):
482
+ label = fn_label(fn)
483
+ name = fn_name(fn)
484
+ print(f"{i:03d} ({label}): {name}")
485
+ print("==================")
486
+
487
+ def _set_draw_functions(self, draw_list: list) -> list[Callable]:
488
+ """Takes a draw list and turns them into function calls.
489
+
490
+ Does not optimize any states. All states are called.
491
+ """
492
+ calls: list[Callable] = []
493
+ last_domain = None
494
+
495
+ for i, (domain, mode, group) in enumerate(draw_list):
496
+ if domain is None:
497
+ # This is a state boundary. See if it *actually* changes state; if so, flush first.
498
+ if mode == "set":
499
+ calls.extend([s.set_state for s in group._expanded_states if s.sets_state])
500
+
501
+ elif mode == "unset":
502
+ calls.extend(reversed([s.unset_state for s in group._expanded_states if s.unsets_state]))
503
+
504
+ continue
505
+
506
+ # Drawable
507
+ bucket = domain.get_drawable_bucket(group)
508
+ if not bucket or bucket.is_empty:
509
+ continue
510
+
511
+ if last_domain is None or domain != last_domain:
512
+ calls.append(self._vao_bind_fn(domain))
513
+
514
+ calls.append(self._draw_bucket_fn(domain, [bucket], geometry_map[mode]))
515
+ last_domain = domain
516
+
517
+ return calls
518
+
519
+ def _dump_draw_list(self) -> None:
520
+ def dump(group: Group, indent: str = '') -> None:
521
+ print(indent, 'Begin group', group)
522
+ for domain in self._domain_registry.values():
523
+ if domain.has_bucket(group):
524
+ # Domain header
525
+ domain_info = repr(domain).split('@')[-1].replace('>', '')
526
+ print(f"{indent} > Domain: {domain.__class__.__name__}@{domain_info}")
527
+
528
+ # Regions
529
+ starts, sizes = domain.vertex_buffers.allocator.get_allocated_regions()
530
+ for start, size in zip(starts, sizes):
531
+ print(f"{indent} - Region start={start:<4} size={size:<4}")
532
+ attribs = ', '.join(domain.attrib_name_buffers.keys())
533
+ print(f"{indent} (Attributes: {attribs})")
534
+ for child in self.group_children.get(group, ()):
535
+ dump(child, indent + ' ')
536
+ print(indent, 'End group', group)
537
+
538
+ print(f'Draw list for {self!r}:')
539
+ for group in self.top_groups:
540
+ dump(group)
541
+
542
+ def _update_draw_list(self) -> None:
543
+ if self._draw_list_dirty:
544
+ draw_list = self._create_draw_list()
545
+ if pyglet.options.optimize_states:
546
+ self._draw_list = self._optimize_draw_list(draw_list)
547
+ else:
548
+ self._draw_list = self._set_draw_functions(draw_list)
549
+ self._draw_list_dirty = False
550
+
551
+ def draw(self) -> None:
552
+ """Draw the batch."""
553
+ self._update_draw_list()
554
+
555
+ for func in self._draw_list:
556
+ func(self._context)
557
+
558
+ self.delete_empty_domains()
559
+
560
+ def draw_subset(self, vertex_lists: Sequence[VertexList | IndexedVertexList]) -> None:
561
+ """Draw only some vertex lists in the batch.
562
+
563
+ The use of this method is highly discouraged, as it is quite
564
+ inefficient. Usually an application can be redesigned so that batches
565
+ can always be drawn in their entirety, using `draw`.
566
+
567
+ The given vertex lists must belong to this batch; behaviour is
568
+ undefined if this condition is not met.
569
+
570
+ Args:
571
+ vertex_lists:
572
+ Vertex lists to draw.
573
+
574
+ """
575
+
576
+ # Horrendously inefficient.
577
+ def visit(group: Group) -> None:
578
+ group.set_state_all(self._context)
579
+
580
+ # Draw domains using this group
581
+ domain_map = self.group_map[group]
582
+ for (_, _, mode, _, _), domain in domain_map.items():
583
+ for alist in vertex_lists:
584
+ if alist.domain is domain:
585
+ alist.draw(mode)
586
+
587
+ # Sort and visit child groups of this group
588
+ children = self.group_children.get(group)
589
+ if children:
590
+ children.sort()
591
+ for child in children:
592
+ if child.visible:
593
+ visit(child)
594
+
595
+ group.unset_state_all(self._context)
596
+
597
+ self.top_groups.sort()
598
+ for top_group in self.top_groups:
599
+ if top_group.visible:
600
+ visit(top_group)