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
@@ -12,7 +12,7 @@ relation to the attribute buffers.
12
12
 
13
13
  Applications can create vertices (and optionally, indices) within a domain
14
14
  with the :py:meth:`VertexDomain.create` method. This returns a
15
- :py:class:`VertexList` representing the list of vertices created. The vertex
15
+ :py:class:`VertexListBase` representing the list of vertices created. The vertex
16
16
  attribute data within the group can be modified, and the changes will be made
17
17
  to the underlying buffers automatically.
18
18
 
@@ -22,41 +22,20 @@ primitives of the same OpenGL primitive mode.
22
22
  """
23
23
  from __future__ import annotations
24
24
 
25
- import ctypes
26
- from typing import TYPE_CHECKING, Any, NoReturn, Sequence, Type
27
-
28
- from _ctypes import Array, _Pointer, _SimpleCData
29
-
30
- from pyglet.gl.gl import (
31
- GL_BYTE,
32
- GL_DOUBLE,
33
- GL_FLOAT,
34
- GL_INT,
35
- GL_SHORT,
36
- GL_UNSIGNED_BYTE,
37
- GL_UNSIGNED_INT,
38
- GL_UNSIGNED_SHORT,
39
- GLint,
40
- GLintptr,
41
- GLsizei,
42
- GLvoid,
43
- glDrawArrays,
44
- glDrawArraysInstanced,
45
- glDrawElements,
46
- glDrawElementsInstanced,
47
- glMultiDrawArrays,
48
- glMultiDrawElements,
49
- )
50
- from pyglet.graphics import allocation, shader, vertexarray
51
- from pyglet.graphics.vertexbuffer import AttributeBufferObject, IndexedBufferObject
52
-
53
- CTypesDataType = Type[_SimpleCData]
54
- CTypesPointer = _Pointer
25
+ from abc import ABC, abstractmethod
26
+ from typing import TYPE_CHECKING, Any, Sequence, Protocol, Iterable, NoReturn
27
+
28
+ import pyglet
29
+ from pyglet.graphics import allocation
30
+ from pyglet.graphics.shader import Attribute, AttributeView, GraphicsAttribute, DataTypeTuple
55
31
 
56
32
  if TYPE_CHECKING:
57
- from pyglet.graphics.allocation import Allocator
58
- from pyglet.graphics.shader import Attribute
59
- from pyglet.graphics.vertexarray import VertexArray
33
+ from ctypes import Array
34
+ from pyglet.customtypes import DataTypes
35
+ from pyglet.graphics.api.base import SurfaceContext
36
+ from pyglet.graphics.instance import InstanceBucket, VertexInstanceBase, BaseInstanceDomain
37
+ from pyglet.graphics.buffer import AttributeBufferObject, IndexedBufferObject
38
+ from pyglet.graphics import GeometryMode, Group
60
39
 
61
40
 
62
41
  def _nearest_pow2(v: int) -> int:
@@ -71,62 +50,41 @@ def _nearest_pow2(v: int) -> int:
71
50
  return v + 1
72
51
 
73
52
 
74
- _c_types = {
75
- GL_BYTE: ctypes.c_byte,
76
- GL_UNSIGNED_BYTE: ctypes.c_ubyte,
77
- GL_SHORT: ctypes.c_short,
78
- GL_UNSIGNED_SHORT: ctypes.c_ushort,
79
- GL_INT: ctypes.c_int,
80
- GL_UNSIGNED_INT: ctypes.c_uint,
81
- GL_FLOAT: ctypes.c_float,
82
- GL_DOUBLE: ctypes.c_double,
83
- }
84
-
85
- _gl_types = {
86
- 'b': GL_BYTE,
87
- 'B': GL_UNSIGNED_BYTE,
88
- 'h': GL_SHORT,
89
- 'H': GL_UNSIGNED_SHORT,
90
- 'i': GL_INT,
91
- 'I': GL_UNSIGNED_INT,
92
- 'f': GL_FLOAT,
93
- 'd': GL_DOUBLE,
94
- }
95
-
96
-
97
53
  def _make_attribute_property(name: str) -> property:
98
- def _attribute_getter(self: VertexList) -> Array[float | int]:
54
+ def _attribute_getter(self: VertexListBase) -> Array[float | int]:
99
55
  buffer = self.domain.attrib_name_buffers[name]
100
- region = buffer.get_region(self.start, self.count)
101
- buffer.invalidate_region(self.start, self.count)
56
+ region = buffer.get_attribute_region(name, self.start, self.count)
57
+ buffer.invalidate_attribute_region(name, self.start, self.count)
102
58
  return region
103
59
 
104
- def _attribute_setter(self: VertexList, data: Any) -> None:
60
+ def _attribute_setter(self: VertexListBase, data: Any) -> None:
105
61
  buffer = self.domain.attrib_name_buffers[name]
106
62
  buffer.set_region(self.start, self.count, data)
107
63
 
108
64
  return property(_attribute_getter, _attribute_setter)
109
65
 
110
66
 
111
- class VertexList:
67
+ class VertexListBase:
112
68
  """A list of vertices within a :py:class:`VertexDomain`.
113
69
 
114
70
  Use :py:meth:`VertexDomain.create` to construct this list.
115
71
  """
116
72
  count: int
117
73
  start: int
118
- domain: VertexDomain | InstancedVertexDomain
74
+ domain: VertexDomainBase
119
75
  indexed: bool = False
120
76
  instanced: bool = False
121
77
  initial_attribs: dict
122
78
 
123
- def __init__(self, domain: VertexDomain, start: int, count: int) -> None: # noqa: D107
79
+ def __init__(self, domain: VertexDomainBase, group: Group, start: int, count: int) -> None: # noqa: D107
124
80
  self.domain = domain
81
+ self.group = group
125
82
  self.start = start
126
83
  self.count = count
127
84
  self.initial_attribs = domain.attribute_meta
85
+ self.bucket = None
128
86
 
129
- def draw(self, mode: int) -> None:
87
+ def draw(self, mode: GeometryMode) -> None:
130
88
  """Draw this vertex list in the given OpenGL mode.
131
89
 
132
90
  Args:
@@ -156,31 +114,10 @@ class VertexList:
156
114
 
157
115
  def delete(self) -> None:
158
116
  """Delete this group."""
159
- self.domain.allocator.dealloc(self.start, self.count)
160
-
161
- def set_instance_source(self, domain: InstancedVertexDomain, instance_attributes: Sequence[str]) -> None:
162
- assert self.instanced is False, "Vertex list is already an instance."
163
- assert list(domain.attribute_names.keys()) == list(self.domain.attribute_names.keys()), \
164
- 'Domain attributes must match.'
117
+ self.domain.vertex_buffers.allocator.dealloc(self.start, self.count)
118
+ self.domain.dealloc_from_group(self)
165
119
 
166
- new_start = domain.safe_alloc(self.count)
167
- for key, current_buffer in self.domain.attrib_name_buffers.items():
168
- new_buffer = domain.attrib_name_buffers[key]
169
- old_data = current_buffer.get_region(self.start, self.count)
170
- if key in instance_attributes:
171
- attrib = domain.attribute_names[key]
172
- count = 1
173
- old_data = old_data[:attrib.count]
174
- else:
175
- count = self.count
176
- new_buffer.set_region(new_start, count, old_data)
177
-
178
- self.domain.allocator.dealloc(self.start, self.count)
179
- self.domain = domain
180
- self.start = new_start
181
- self.instanced = True
182
-
183
- def migrate(self, domain: VertexDomain | InstancedVertexDomain) -> None:
120
+ def migrate(self, domain: VertexDomainBase, group: Group) -> None:
184
121
  """Move this group from its current domain and add to the specified one.
185
122
 
186
123
  Attributes on domains must match.
@@ -189,275 +126,422 @@ class VertexList:
189
126
  Args:
190
127
  domain:
191
128
  Domain to migrate this vertex list to.
192
-
129
+ group:
130
+ The group this vertex list belongs to.
193
131
  """
194
- assert list(domain.attribute_names.keys()) == list(self.domain.attribute_names.keys()), \
132
+ assert list(domain.attribute_names.keys()) == list(self.domain.attribute_names.keys()), (
195
133
  'Domain attributes must match.'
134
+ )
196
135
 
197
136
  new_start = domain.safe_alloc(self.count)
198
- for name, old_buffer in self.domain.attrib_name_buffers.items():
199
- new_buffer = domain.attrib_name_buffers[name]
200
- old_data = old_buffer.get_region(self.start, self.count)
201
- new_buffer.set_region(new_start, self.count, old_data)
202
-
203
- self.domain.allocator.dealloc(self.start, self.count)
137
+ # Copy data to new stream.
138
+ self.domain.dealloc_from_group(self)
139
+ self.domain.vertex_buffers.copy_data(new_start, domain.vertex_buffers, self.start, self.count)
140
+ self.domain.vertex_buffers.allocator.dealloc(self.start, self.count)
204
141
  self.domain = domain
205
142
  self.start = new_start
143
+ domain.alloc_to_group(self, group)
144
+ assert self.bucket is not None
145
+
146
+ def update_group(self, group: Group) -> None:
147
+ current_bucket = self.bucket
148
+ self.domain.dealloc_from_group(self)
149
+ new_bucket = self.domain.alloc_to_group(self, group)
150
+ assert new_bucket != current_bucket, "Changing group resulted in the same bucket."
206
151
 
207
152
  def set_attribute_data(self, name: str, data: Any) -> None:
208
- buffer = self.domain.attrib_name_buffers[name]
209
- count = self.count
153
+ stream = self.domain.attrib_name_buffers[name]
154
+ buffer = stream.attrib_name_buffers[name]
210
155
 
211
- array_start = buffer.count * self.start
212
- array_end = buffer.count * count + array_start
156
+ array_start = buffer.element_count * self.start
157
+ array_end = buffer.element_count * self.count + array_start
213
158
  try:
214
159
  buffer.data[array_start:array_end] = data
215
- buffer.invalidate_region(self.start, count)
160
+ buffer.invalidate_region(self.start, self.count)
216
161
  except ValueError:
217
162
  msg = f"Invalid data size for '{name}'. Expected {array_end - array_start}, got {len(data)}."
218
163
  raise ValueError(msg) from None
219
164
 
220
- def add_instance(self, **kwargs: Any) -> VertexInstance:
221
- assert self.instanced
222
- self.domain._instances += 1 # noqa: SLF001
223
165
 
224
- instance_id = self.domain._instances # noqa: SLF001
166
+ class InstanceVertexListBase(VertexListBase):
167
+ """A list of vertices within an :py:class:`InstancedVertexDomain` that are not indexed."""
168
+ domain: InstancedVertexDomainBase
169
+ instanced: bool = True
225
170
 
226
- start = self.domain.safe_alloc_instance(3)
171
+ def __init__(self, domain: VertexDomainBase, group: Group, start: int, count: int, bucket: InstanceBucket) -> None: # noqa: D107
172
+ super().__init__(domain, group, start, count)
173
+ self.instance_bucket = bucket
174
+ self.instance_bucket.create_instance()
227
175
 
228
- for buffer, attribute in self.domain.buffer_attributes:
229
- if attribute.instance:
230
- assert attribute.name in kwargs, (f"{attribute.name} is defined as an instance attribute, "
231
- f"keyword argument not found.")
232
- buffer.set_region(instance_id - 1, 1, kwargs[attribute.name])
176
+ def create_instance(self, **attributes: Any) -> VertexInstanceBase:
177
+ return self.instance_bucket.create_instance(**attributes)
233
178
 
234
- return self.domain._vertexinstance_class(self, instance_id, start) # noqa: SLF001
179
+ def set_attribute_data(self, name: str, data: Any) -> None:
180
+ if self.initial_attribs[name].fmt.is_instanced:
181
+ stream = self.instance_bucket.stream
182
+ count = 1
183
+ start = 0
184
+ else:
185
+ stream = self.domain.attrib_name_buffers[name]
186
+ count = self.count
187
+ start = self.start
188
+ buffer = stream.attrib_name_buffers[name]
235
189
 
236
- def delete_instance(self, instance: VertexInstance) -> None:
237
- assert self.instanced
238
- if instance.id != self.domain._instances: # noqa: SLF001
239
- msg = "Only the last instance added can be removed."
240
- raise Exception(msg)
190
+ array_start = buffer.element_count * start
191
+ array_end = buffer.element_count * count + array_start
192
+ try:
193
+ buffer.data[array_start:array_end] = data
194
+ buffer.invalidate_region(start, count)
195
+ except ValueError:
196
+ msg = f"Invalid data size for '{buffer}'. Expected {array_end - array_start}, got {len(data)}."
197
+ raise ValueError(msg) from None
241
198
 
242
- self.domain._instances -= 1 # noqa: SLF001
243
199
 
244
- self.domain.instance_allocator.dealloc(instance.start, 3)
200
+ class _IndexSupportBase:
201
+ domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase
202
+ start: int
203
+ count: int
204
+ bucket: None
245
205
 
206
+ def migrate(self, domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase, group: Group):
207
+ self.domain.dealloc_from_group(self)
208
+ new_start = domain.safe_alloc(self.count)
209
+ # Copy data to new stream.
210
+ self.domain.vertex_buffers.copy_data(new_start, domain.vertex_buffers, self.start, self.count)
211
+ self.domain.vertex_buffers.allocator.dealloc(self.start, self.count)
212
+ self.domain = domain
213
+ self.start = new_start
246
214
 
247
- class IndexedVertexList(VertexList):
248
- """A list of vertices within an :py:class:`IndexedVertexDomain` that are indexed.
215
+ class _LocalIndexSupport(_IndexSupportBase):
216
+ """When BaseVertex is supported by the version, this class will be mixed in.
249
217
 
250
- Use :py:meth:`IndexedVertexDomain.create` to construct this list.
218
+ Will allow the class to use local index values instead of incrementing each mesh.
251
219
  """
252
- domain: IndexedVertexDomain | InstancedIndexedVertexDomain
253
- indexed: bool = True
220
+ __slots__: tuple[str, ...] = ()
221
+
222
+ supports_base_vertex: bool = True
254
223
 
224
+ domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase
255
225
  index_count: int
256
226
  index_start: int
257
227
 
258
- def __init__(self, domain: IndexedVertexDomain, start: int, count: int, index_start: int, # noqa: D107
259
- index_count: int) -> None:
260
- super().__init__(domain, start, count)
261
- self.index_start = index_start
262
- self.index_count = index_count
228
+ @property
229
+ def indices(self) -> list[int]:
230
+ return self.domain.index_stream.get_region(self.index_start, self.index_count)[:]
263
231
 
264
- def delete(self) -> None:
265
- """Delete this group."""
266
- super().delete()
267
- self.domain.index_allocator.dealloc(self.index_start, self.index_count)
232
+ @indices.setter
233
+ def indices(self, local: Sequence[int]) -> None: # type: ignore[override]
234
+ self.domain.index_stream.set_region(self.index_start, self.index_count, local)
268
235
 
269
- def migrate(self, domain: IndexedVertexDomain | InstancedIndexedVertexDomain) -> None:
270
- """Move this group from its current indexed domain and add to the specified one.
236
+ def migrate(self, domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase, group: Group) -> None: # type: ignore[override]
237
+ old_dom = self.domain
238
+ src_idx_start = self.index_start
239
+ src_idx_count = self.index_count
271
240
 
272
- Attributes on domains must match. (In practice, used
273
- to change parent state of some vertices).
241
+ super().migrate(domain, group)
274
242
 
275
- Args:
276
- domain:
277
- Indexed domain to migrate this vertex list to.
278
- """
279
- old_start = self.start
280
- old_domain = self.domain
281
- super().migrate(domain)
243
+ data = old_dom.index_stream.get_region(src_idx_start, src_idx_count)
244
+ old_dom.index_stream.allocator.dealloc(src_idx_start, src_idx_count)
282
245
 
283
- # Note: this code renumber the indices of the *original* domain
284
- # because the vertices are in a new position in the new domain
285
- if old_start != self.start:
286
- diff = self.start - old_start
287
- old_indices = old_domain.index_buffer.get_region(self.index_start, self.index_count)
288
- old_domain.index_buffer.set_region(self.index_start, self.index_count, [i + diff for i in old_indices])
246
+ new_idx_start = self.domain.safe_index_alloc(src_idx_count)
247
+ self.domain.index_stream.set_region(new_idx_start, src_idx_count, data)
248
+ self.index_start = new_idx_start
249
+ domain.alloc_to_group(self, group) # Allocate after new index start.
289
250
 
290
- # copy indices to new domain
291
- old_array = old_domain.index_buffer.get_region(self.index_start, self.index_count)
292
- # must delloc before calling safe_index_alloc or else problems when same
293
- # batch is migrated to because index_start changes after dealloc
294
- old_domain.index_allocator.dealloc(self.index_start, self.index_count)
295
251
 
296
- new_start = self.domain.safe_index_alloc(self.index_count)
297
- self.domain.index_buffer.set_region(new_start, self.index_count, old_array)
252
+ class _RunningIndexSupport(_IndexSupportBase):
253
+ """Used to mixin an IndexedVertexListBase class.
298
254
 
299
- self.index_start = new_start
255
+ Keeps an incrementing count for indices in the buffer.
256
+ """
300
257
 
301
- def set_instance_source(self, domain: IndexedVertexDomain | InstancedIndexedVertexDomain,
302
- instance_attributes: Sequence[str]) -> None:
303
- assert self.instanced is False, "IndexedVertexList is already an instance."
304
- old_start = self.start
305
- old_domain = self.domain
306
- super().set_instance_source(domain, instance_attributes)
258
+ domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase
259
+ index_count: int
260
+ index_start: int
261
+ start: int
307
262
 
308
- assert list(domain.attribute_names.keys()) == list(self.domain.attribute_names.keys()), \
309
- 'Domain attributes must match.'
263
+ supports_base_vertex: bool = False
264
+
265
+ @property
266
+ def indices(self) -> list[int]:
267
+ stored = self.domain.index_stream.get_region(self.index_start, self.index_count)
268
+ base = self.start
269
+ return [i - base for i in stored]
270
+
271
+ @indices.setter
272
+ def indices(self, local: Sequence[int]) -> None:
273
+ base: int = self.start
274
+ stored: list[int] = [i + base for i in local]
275
+ self.domain.index_stream.set_region(self.index_start, self.index_count, stored)
310
276
 
311
- # Note: this code renumber the indices of the *original* domain
312
- # because the vertices are in a new position in the new domain
313
- if old_start != self.start:
314
- diff = self.start - old_start
315
- old_indices = old_domain.index_buffer.get_region(self.index_start, self.index_count)
316
- old_domain.index_buffer.set_region(self.index_start, self.index_count, [i + diff for i in old_indices])
277
+ def migrate(self, domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase, group: Group) -> None:
278
+ old_dom = self.domain
279
+ old_start: int = self.start
280
+ src_idx_start = self.index_start
281
+ src_idx_count = self.index_count
317
282
 
318
- # copy indices to new domain
319
- old_array = old_domain.index_buffer.get_region(self.index_start, self.index_count)
320
- # must delloc before calling safe_index_alloc or else problems when same
321
- # batch is migrated to because index_start changes after dealloc
322
- old_domain.index_allocator.dealloc(self.index_start, self.index_count)
283
+ super().migrate(domain, group)
323
284
 
324
- new_start = self.domain.safe_index_alloc(self.index_count)
325
- self.domain.index_buffer.set_region(new_start, self.index_count, old_array)
285
+ data = old_dom.index_stream.get_region(src_idx_start, src_idx_count)
286
+ old_dom.index_stream.allocator.dealloc(src_idx_start, src_idx_count)
326
287
 
327
- self.index_start = new_start
288
+ delta: int = self.start - old_start
289
+ if delta:
290
+ data = [i + delta for i in data]
291
+
292
+ new_idx_start = self.domain.safe_index_alloc(src_idx_count)
293
+ self.domain.index_stream.set_region(new_idx_start, src_idx_count, data)
294
+ self.index_start = new_idx_start
295
+ domain.alloc_to_group(self, group) # Allocate after new index start.
296
+
297
+
298
+ class IndexedVertexListBase(VertexListBase):
299
+ """A list of vertices within an :py:class:`IndexedVertexDomainBase` that are indexed.
300
+
301
+ Use :py:meth:`IndexedVertexDomainBase.create` to construct this list.
302
+ """
303
+ domain: IndexedVertexDomainBase
304
+ indexed: bool = True
305
+
306
+ index_count: int
307
+ index_start: int
308
+
309
+ def __init__(self, domain: IndexedVertexDomainBase, group: Group, start: int, count: int, index_start: int, # noqa: D107
310
+ index_count: int) -> None:
311
+ super().__init__(domain, group, start, count)
312
+ self.index_start = index_start
313
+ self.index_count = index_count
314
+
315
+ def delete(self) -> None:
316
+ """Delete this group."""
317
+ super().delete()
318
+ self.domain.index_stream.dealloc(self.index_start, self.index_count)
328
319
 
329
320
  @property
330
321
  def indices(self) -> list[int]:
331
322
  """Array of index data."""
332
- start = self.start
333
- return [i - start for i in self.domain.index_buffer.get_region(self.index_start, self.index_count)]
323
+ return self.domain.index_stream.get_region(self.index_start, self.index_count)
334
324
 
335
325
  @indices.setter
336
326
  def indices(self, data: Sequence[int]) -> None:
337
- start = self.start
338
327
  # The vertex data is offset in the buffer, so offset the index values to match. Ex:
339
328
  # vertex_buffer: [_, _, _, _, 1, 2, 3, 4]
340
- self.domain.index_buffer.set_region(self.index_start, self.index_count, tuple(i + start for i in data))
329
+ self.domain.index_stream.set_region(self.index_start, self.index_count, data)
341
330
 
342
331
 
343
- class VertexInstance:
344
- id: int
345
- start: int
346
- _vertex_list: VertexList | IndexedVertexList
332
+ class InstanceIndexedVertexListBase(VertexListBase):
333
+ """A list of vertices within an :py:class:`IndexedVertexDomain` that are indexed.
347
334
 
348
- def __init__(self, vertex_list: VertexList | IndexedVertexList, instance_id: int, start: int) -> None:
349
- self.id = instance_id
350
- self.start = start
351
- self._vertex_list = vertex_list
335
+ Use :py:meth:`IndexedVertexDomain.create` to construct this list.
336
+ """
337
+ domain: IndexedVertexDomainBase | InstancedIndexedVertexDomainBase
338
+ indexed: bool = True
339
+ instanced: bool = True
352
340
 
353
- @property
354
- def domain(self) -> InstancedVertexDomain | InstancedIndexedVertexDomain:
355
- return self._vertex_list.domain
341
+ index_count: int
342
+ index_start: int
343
+
344
+ instance_bucket: InstanceBucket
345
+ supports_base_vertex: bool = False
346
+
347
+ def __init__(self, domain: InstancedIndexedVertexDomainBase, group: Group, start: int, count: int,
348
+ index_start: int, index_count: int, index_type: DataTypes, base_vertex: int,
349
+ instance_bucket: InstanceBucket) -> None:
350
+ self.index_start = index_start
351
+ self.index_count = index_count
352
+ self.index_type = index_type
353
+ self.base_vertex = base_vertex
354
+ self.instance_bucket = instance_bucket
355
+ self.instance_bucket.create_instance()
356
+ self.start_base_vertex = start if self.supports_base_vertex else 0
357
+ super().__init__(domain, group, start, count)
356
358
 
357
359
  def delete(self) -> None:
358
- self._vertex_list.delete_instance(self)
359
- self._vertex_list = None
360
+ """Delete this group."""
361
+ raise Exception
362
+ super().delete()
363
+ self.domain.index_allocator.dealloc(self.index_start, self.index_count)
364
+
365
+ def migrate(self, domain: InstancedIndexedVertexDomainBase) -> None:
366
+ old_domain = self.domain
367
+
368
+ # Moved vertex data here.
369
+ super().migrate(domain)
370
+
371
+ # Remove from bucket and enter into new bucket.
372
+ new_bucket = domain.instance_domain.get_elements_bucket(mode=0,
373
+ first_index=self.index_start,
374
+ index_count=self.index_count,
375
+ index_type="I")
360
376
 
377
+ # Move instance data.
378
+ old_domain.instance_domain.move_all(self.instance_bucket, new_bucket)
361
379
 
362
- class VertexDomain:
380
+ def create_instance(self, **attributes: Any) -> None:
381
+ return self.instance_bucket.create_instance(**attributes)
382
+
383
+ def set_attribute_data(self, name: str, data: Any) -> None:
384
+ if self.initial_attribs[name].fmt.is_instanced:
385
+ stream = self.instance_bucket.stream
386
+ count = 1
387
+ start = 0
388
+ else:
389
+ stream = self.domain.attrib_name_buffers[name]
390
+ count = self.count
391
+ start = self.start
392
+ buffer = stream.attrib_name_buffers[name]
393
+
394
+ array_start = buffer.element_count * start
395
+ array_end = buffer.element_count * count + array_start
396
+ try:
397
+ buffer.data[array_start:array_end] = data
398
+ buffer.invalidate_region(start, count)
399
+ except ValueError:
400
+ msg = f"Invalid data size for '{buffer}'. Expected {array_end - array_start}, got {len(data)}."
401
+ raise ValueError(msg) from None
402
+
403
+ def dealloc_from_group(self, vertex_list):
404
+ """Removes a vertex list from a specific state in this domain."""
405
+ vertex_list.bucket.remove_vertex_list(vertex_list)
406
+
407
+ class VertexDomainBase(ABC):
363
408
  """Management of a set of vertex lists.
364
409
 
365
410
  Construction of a vertex domain is usually done with the
366
411
  :py:func:`create_domain` function.
367
412
  """
368
413
 
369
- attribute_meta: dict[str, dict[str, Any]]
370
- allocator: Allocator
414
+ attribute_meta: dict[str, Attribute]
371
415
  buffer_attributes: list[tuple[AttributeBufferObject, Attribute]]
372
- vao: VertexArray
373
416
  attribute_names: dict[str, Attribute]
374
- attrib_name_buffers: dict[str, AttributeBufferObject]
417
+ attrib_name_buffers: dict[str, VertexStream | InstanceStream]
418
+ vertex_stream: VertexStream
375
419
 
376
420
  _property_dict: dict[str, property]
377
421
  _vertexlist_class: type
378
422
 
379
- _initial_count: int = 16
380
- _vertex_class: type[VertexList] = VertexList
423
+ _vertex_class: type[VertexListBase] = VertexListBase
381
424
 
382
- def __init__(self, attribute_meta: dict[str, dict[str, Any]]) -> None: # noqa: D107
425
+ def __init__(self, context: SurfaceContext, initial_count: int, attribute_meta: dict[str, Attribute]) -> None:
426
+ self._context = context or pyglet.graphics.api.core.current_context
383
427
  self.attribute_meta = attribute_meta
384
- self.allocator = allocation.Allocator(self._initial_count)
385
- self.vao = vertexarray.VertexArray()
428
+ self.attrib_name_buffers = {}
429
+ self._supports_multi_draw = self._has_multi_draw_extension(self._context)
430
+
431
+ # Separate attributes.
432
+ self.per_vertex: list[Attribute] = []
433
+ self.per_instance: list[Attribute] = []
434
+ for attrib in attribute_meta.values():
435
+ if not attrib.fmt.is_instanced:
436
+ self.per_vertex.append(attrib)
437
+ else:
438
+ self.per_instance.append(attrib)
386
439
 
387
- self.attribute_names = {} # name: attribute
388
- self.buffer_attributes = [] # list of (buffer, attribute)
389
- self.attrib_name_buffers = {} # dict of AttributeName: AttributeBufferObject (for VertexLists)
440
+ self.vertex_buffers = None
390
441
 
391
- self._property_dict = {} # name: property(_getter, _setter)
442
+ # This function should set vertex_buffers
443
+ self._streams = self._create_streams(initial_count)
444
+ self.vao = self._create_vao()
445
+ self._vertex_buckets = {}
392
446
 
393
- for name, meta in attribute_meta.items():
394
- assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid attribute format for '{name}'."
395
- location = meta['location']
396
- count = meta['count']
397
- gl_type = _gl_types[meta['format'][0]]
398
- normalize = 'n' in meta['format']
399
- instanced = meta['instance']
447
+ for name, attrib in attribute_meta.items():
448
+ if not attrib.fmt.is_instanced:
449
+ self.attrib_name_buffers[name] = self.vertex_buffers
400
450
 
401
- self.attribute_names[name] = attribute = shader.Attribute(name, location, count, gl_type, normalize,
402
- instanced)
451
+ # Make a custom VertexListBase class w/ properties for each attribute
452
+ self._vertexlist_class = self._create_vertex_class()
403
453
 
404
- # Create buffer:
405
- self.attrib_name_buffers[name] = buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity,
406
- attribute)
407
- # TODO: use persistent buffer if we have GL support for it:
408
- # attribute.buffer = PersistentBufferObject(attribute.stride * self.allocator.capacity, attribute, self.vao)
454
+ @abstractmethod
455
+ def _has_multi_draw_extension(self, ctx: SurfaceContext) -> bool:
456
+ raise NotImplementedError
409
457
 
410
- self.buffer_attributes.append((buffer, attribute))
458
+ @abstractmethod
459
+ def _create_streams(self, size: int) -> list[VertexStream | IndexStream | InstanceStream]:
460
+ ...
411
461
 
412
- # Create custom property to be used in the VertexList:
413
- self._property_dict[attribute.name] = _make_attribute_property(name)
414
-
415
- # Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
416
- self._vertexlist_class = type(self._vertex_class.__name__, (self._vertex_class,), self._property_dict)
462
+ @abstractmethod
463
+ def _create_vao(self) -> VertexArrayBinding:
464
+ ...
417
465
 
466
+ def bind_vao(self) -> None:
467
+ """Binds the VAO as well as commit any pending buffer changes to the GPU."""
418
468
  self.vao.bind()
419
- for buffer, attribute in self.buffer_attributes:
420
- buffer.bind()
421
- attribute.enable()
422
- attribute.set_pointer(buffer.ptr)
423
- if attribute.instance:
424
- attribute.set_divisor()
425
- self.vao.unbind()
469
+ for stream in self._streams:
470
+ stream.commit()
471
+
472
+ @property
473
+ def attribute_names(self):
474
+ return self.vertex_buffers.attribute_names
426
475
 
427
476
  def safe_alloc(self, count: int) -> int:
428
477
  """Allocate vertices, resizing the buffers if necessary."""
429
- try:
430
- return self.allocator.alloc(count)
431
- except allocation.AllocatorMemoryException as e:
432
- capacity = _nearest_pow2(e.requested_capacity)
433
- for buffer, _ in self.buffer_attributes:
434
- buffer.resize(capacity * buffer.stride)
435
- self.allocator.set_capacity(capacity)
436
- return self.allocator.alloc(count)
478
+ return self.vertex_buffers.alloc(count)
437
479
 
438
480
  def safe_realloc(self, start: int, count: int, new_count: int) -> int:
439
481
  """Reallocate vertices, resizing the buffers if necessary."""
440
- try:
441
- return self.allocator.realloc(start, count, new_count)
442
- except allocation.AllocatorMemoryException as e:
443
- capacity = _nearest_pow2(e.requested_capacity)
444
- for buffer, _ in self.buffer_attributes:
445
- buffer.resize(capacity * buffer.stride)
446
- self.allocator.set_capacity(capacity)
447
- return self.allocator.realloc(start, count, new_count)
482
+ return self.vertex_buffers.realloc(start, count, new_count)
448
483
 
449
- def create(self, count: int, index_count: int | None = None) -> VertexList: # noqa: ARG002
450
- """Create a :py:class:`VertexList` in this domain.
484
+ def create(self, group: Group, count: int, indices: Sequence[int] | None = None) -> VertexListBase: # noqa: ARG002
485
+ """Create a :py:class:`VertexListBase` in this domain.
451
486
 
452
487
  Args:
488
+ group:
489
+ The :py:class:`Group` the resulting vertex list will be drawn with.
453
490
  count:
454
491
  Number of vertices to create.
455
- index_count:
492
+ indices:
456
493
  Ignored for non indexed VertexDomains
457
494
  """
458
495
  start = self.safe_alloc(count)
459
- return self._vertexlist_class(self, start, count)
496
+ vlist = self._vertexlist_class(self, group, start, count)
497
+ self.alloc_to_group(vlist, group)
498
+ return vlist
460
499
 
500
+ def get_drawable_bucket(self, group: Group) -> VertexGroupBucket | None:
501
+ """Get a bucket that exists and has vertices to draw (not empty)."""
502
+ bucket = self._vertex_buckets.get(group)
503
+ if bucket is None or (bucket and bucket.is_empty):
504
+ return None
505
+
506
+ return bucket
507
+
508
+ def alloc_to_group(self, vertex_list, group) -> VertexGroupBucket:
509
+ """Assigns a vertex list to a specific state in this domain.
510
+
511
+ A state bucket does not allocate any vertices or allocates any GPU resources, it is simply to track the
512
+ data required for drawing in a specific state.
513
+
514
+ Args:
515
+ vertex_list:
516
+ The vertex list to allocate.
517
+ group:
518
+ The group affecting the vertices.
519
+
520
+ Returns:
521
+ The new state bucket object.
522
+ """
523
+ state_bucket = self._get_state_bucket(group)
524
+ state_bucket.add_vertex_list(vertex_list)
525
+ vertex_list.bucket = state_bucket
526
+ return state_bucket
527
+
528
+ def dealloc_from_group(self, vertex_list):
529
+ """Removes a vertex list from a specific state in this domain."""
530
+ assert vertex_list.bucket is not None
531
+ vertex_list.bucket.remove_vertex_list(vertex_list)
532
+ vertex_list.bucket = None
533
+
534
+ def _get_state_bucket(self, group: Group) -> VertexGroupBucket:
535
+ """Get a drawable bucket to assign vertex list information to a specific group."""
536
+ bucket = self._vertex_buckets.get(group)
537
+ if bucket is None:
538
+ bucket = self._vertex_buckets[group] = VertexGroupBucket()
539
+ return bucket
540
+
541
+ def has_bucket(self, group: Group) -> bool:
542
+ return group in self._vertex_buckets
543
+
544
+ @abstractmethod
461
545
  def draw(self, mode: int) -> None:
462
546
  """Draw all vertices in the domain.
463
547
 
@@ -469,26 +553,12 @@ class VertexDomain:
469
553
  OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
470
554
 
471
555
  """
472
- self.vao.bind()
473
- for buffer, _ in self.buffer_attributes:
474
- buffer.commit()
475
-
476
- starts, sizes = self.allocator.get_allocated_regions()
477
- primcount = len(starts)
478
- if primcount == 0:
479
- pass
480
- elif primcount == 1:
481
- # Common case
482
- glDrawArrays(mode, starts[0], sizes[0])
483
- else:
484
- starts = (GLint * primcount)(*starts)
485
- sizes = (GLsizei * primcount)(*sizes)
486
- glMultiDrawArrays(mode, starts, sizes, primcount)
487
556
 
488
- def draw_subset(self, mode: int, vertex_list: VertexList) -> None:
489
- """Draw a specific VertexList in the domain.
557
+ @abstractmethod
558
+ def draw_subset(self, mode: GeometryMode, vertex_list: VertexListBase) -> None:
559
+ """Draw a specific VertexListBase in the domain.
490
560
 
491
- The `vertex_list` parameter specifies a :py:class:`VertexList`
561
+ The `vertex_list` parameter specifies a :py:class:`VertexListBase`
492
562
  to draw. Only primitives in that list will be drawn.
493
563
 
494
564
  Args:
@@ -498,98 +568,73 @@ class VertexDomain:
498
568
  Vertex list to draw.
499
569
 
500
570
  """
501
- self.vao.bind()
502
- for buffer, _ in self.buffer_attributes:
503
- buffer.commit()
504
-
505
- glDrawArrays(mode, vertex_list.start, vertex_list.count)
506
571
 
507
572
  @property
508
573
  def is_empty(self) -> bool:
509
- return not self.allocator.starts
574
+ """If the domain has no vertices."""
575
+ return not self.vertex_buffers.allocator.starts
510
576
 
511
577
  def __repr__(self) -> str:
512
- return f'<{self.__class__.__name__}@{id(self):x} {self.allocator}>'
513
-
578
+ return f'<{self.__class__.__name__}@{id(self):x} vertex_alloc={self.vertex_buffers.allocator}>'
514
579
 
515
- def _make_instance_attribute_property(name: str) -> property:
516
- def _attribute_getter(self: VertexInstance) -> Array[CTypesDataType]:
517
- buffer = self.domain.attrib_name_buffers[name]
518
- region = buffer.get_region(self.id - 1, 1)
519
- buffer.invalidate_region(self.id - 1, 1)
520
- return region
521
580
 
522
- def _attribute_setter(self: VertexInstance, data: Any) -> None:
523
- buffer = self.domain.attrib_name_buffers[name]
524
- buffer.set_region(self.id - 1, 1, data)
525
-
526
- return property(_attribute_getter, _attribute_setter)
527
-
528
-
529
- def _make_restricted_instance_attribute_property(name: str) -> property:
530
- def _attribute_getter(self: VertexInstance) -> Array[CTypesDataType]:
531
- buffer = self.domain.attrib_name_buffers[name]
532
- return buffer.get_region(self.id - 1, 1)
533
-
534
- def _attribute_setter(_self: VertexInstance, _data: Any) -> NoReturn:
535
- msg = f"Attribute '{name}' is not an instanced attribute."
536
- raise Exception(msg)
537
-
538
- return property(_attribute_getter, _attribute_setter)
539
-
540
-
541
- class InstancedVertexDomain(VertexDomain): # noqa: D101
542
- instance_allocator: Allocator
543
- _instances: int
544
- _instance_properties: dict[str, property]
545
- _vertexinstance_class: type
581
+ class IndexedVertexDomainBase(VertexDomainBase):
582
+ """Management of a set of indexed vertex lists.
546
583
 
547
- def __init__(self, attribute_meta: dict[str, dict[str, Any]]) -> None:
548
- super().__init__(attribute_meta)
549
- self._instances = 1
550
- self.instance_allocator = allocation.Allocator(self._initial_count)
584
+ Construction of an indexed vertex domain is usually done with the
585
+ :py:func:`create_domain` function.
586
+ """
587
+ _initial_index_count = 16
588
+ _vertex_class = IndexedVertexListBase
589
+ index_stream: IndexStream
590
+
591
+ def __init__(self, context: SurfaceContext, initial_count: int, attribute_meta: dict[str, Attribute],
592
+ index_type: DataTypes = "I") -> None:
593
+ self.index_type = index_type
594
+ self._supports_base_vertex = context.get_info().have_extension("GL_ARB_draw_elements_base_vertex")
595
+ super().__init__(context, initial_count, attribute_meta)
596
+
597
+ def get_group_bucket(self, group: Group) -> IndexedVertexGroupBucket:
598
+ """Get a drawable bucket to assign vertex list information to a specific group."""
599
+ bucket = self._vertex_buckets.get(group)
600
+ if bucket is None:
601
+ bucket = self._vertex_buckets[group] = IndexedVertexGroupBucket()
602
+ return bucket
551
603
 
552
- self._instance_properties = {}
553
- for name, attribute in self.attribute_names.items():
554
- if attribute.instance:
555
- self._instance_properties[name] = _make_instance_attribute_property(name)
556
- else:
557
- self._instance_properties[name] = _make_restricted_instance_attribute_property(name)
604
+ def safe_index_alloc(self, count: int) -> int:
605
+ """Allocate indices, resizing the buffers if necessary."""
606
+ return self.index_stream.alloc(count)
558
607
 
559
- self._vertexinstance_class = type('VertexInstance', (VertexInstance,), self._instance_properties)
608
+ def safe_index_realloc(self, start: int, count: int, new_count: int) -> int:
609
+ """Reallocate indices, resizing the buffers if necessary."""
610
+ return self.index_stream.realloc(start, count, new_count)
560
611
 
561
- def safe_alloc_instance(self, count: int) -> int:
562
- try:
563
- return self.instance_allocator.alloc(count)
564
- except allocation.AllocatorMemoryException as e:
565
- capacity = _nearest_pow2(e.requested_capacity)
566
- for buffer, attribute in self.buffer_attributes:
567
- if attribute.instance:
568
- buffer.resize(capacity * buffer.stride)
569
- self.instance_allocator.set_capacity(capacity)
570
- return self.instance_allocator.alloc(count)
612
+ def create(self, group: Group, count: int, indices: Sequence[int] | None = None) -> IndexedVertexListBase:
613
+ """Create an :py:class:`IndexedVertexList` in this domain.
571
614
 
572
- def safe_alloc(self, count: int) -> int:
573
- """Allocate vertices, resizing the buffers if necessary."""
574
- try:
575
- return self.allocator.alloc(count)
576
- except allocation.AllocatorMemoryException as e:
577
- capacity = _nearest_pow2(e.requested_capacity)
578
- for buffer, _ in self.buffer_attributes:
579
- buffer.resize(capacity * buffer.stride)
580
- self.allocator.set_capacity(capacity)
581
- return self.allocator.alloc(count)
615
+ Args:
616
+ group:
617
+ The :py:class:`Group` the resulting vertex list will be drawn with.
618
+ count:
619
+ Number of vertices to create
620
+ indices:
621
+ The indices used for this vertex list.
582
622
 
583
- def safe_realloc(self, start: int, count: int, new_count: int) -> int:
584
- """Reallocate vertices, resizing the buffers if necessary."""
585
- try:
586
- return self.allocator.realloc(start, count, new_count)
587
- except allocation.AllocatorMemoryException as e:
588
- capacity = _nearest_pow2(e.requested_capacity)
589
- for buffer, _ in self.buffer_attributes:
590
- buffer.resize(capacity * buffer.stride)
591
- self.allocator.set_capacity(capacity)
592
- return self.allocator.realloc(start, count, new_count)
623
+ """
624
+ index_count = len(indices)
625
+ start = self.safe_alloc(count)
626
+ index_start = self.safe_index_alloc(index_count)
627
+ vertex_list = self._vertexlist_class(self, group, start, count, index_start, index_count)
628
+ vertex_list.indices = indices # Move into class at some point?
629
+ self.alloc_to_group(vertex_list, group)
630
+ return vertex_list
631
+
632
+ def _get_state_bucket(self, group: Group) -> IndexedVertexGroupBucket:
633
+ """Get a drawable bucket to assign vertex list information to a specific group."""
634
+ bucket = self._vertex_buckets.get(group)
635
+ if bucket is None:
636
+ bucket = self._vertex_buckets[group] = IndexedVertexGroupBucket()
637
+ return bucket
593
638
 
594
639
  def draw(self, mode: int) -> None:
595
640
  """Draw all vertices in the domain.
@@ -602,17 +647,11 @@ class InstancedVertexDomain(VertexDomain): # noqa: D101
602
647
  OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
603
648
 
604
649
  """
605
- self.vao.bind()
606
- for buffer, _ in self.buffer_attributes:
607
- buffer.commit()
608
650
 
609
- starts, sizes = self.allocator.get_allocated_regions()
610
- glDrawArraysInstanced(mode, starts[0], sizes[0], self._instances)
651
+ def draw_subset(self, mode: GeometryMode, vertex_list: IndexedVertexListBase) -> None:
652
+ """Draw a specific IndexedVertexListBase in the domain.
611
653
 
612
- def draw_subset(self, mode: int, vertex_list: VertexList) -> None:
613
- """Draw a specific VertexList in the domain.
614
-
615
- The `vertex_list` parameter specifies a :py:class:`VertexList`
654
+ The `vertex_list` parameter specifies a :py:class:`IndexedVertexListBase`
616
655
  to draw. Only primitives in that list will be drawn.
617
656
 
618
657
  Args:
@@ -621,227 +660,345 @@ class InstancedVertexDomain(VertexDomain): # noqa: D101
621
660
  vertex_list:
622
661
  Vertex list to draw.
623
662
  """
624
- self.vao.bind()
625
- for buffer, _ in self.buffer_attributes:
626
- buffer.commit()
627
663
 
628
- glDrawArraysInstanced(mode, vertex_list.start, vertex_list.count, self._instances)
629
-
630
- @property
631
- def is_empty(self) -> bool:
632
- return not self.allocator.starts
633
664
 
665
+ class InstancedVertexDomainBase(VertexDomainBase):
666
+ def __init__(self, context: SurfaceContext, initial_count: int, attribute_meta: dict[str, Attribute]) -> None:
667
+ super().__init__(context, initial_count, attribute_meta)
668
+ self.instance_domain = self.create_instance_domain(initial_count)
669
+ self._instance_map = {}
634
670
 
635
- class IndexedVertexDomain(VertexDomain):
636
- """Management of a set of indexed vertex lists.
671
+ @abstractmethod
672
+ def create_instance_domain(self, size: int) -> BaseInstanceDomain:
673
+ ...
637
674
 
638
- Construction of an indexed vertex domain is usually done with the
639
- :py:func:`create_domain` function.
640
- """
641
- index_allocator: Allocator
642
- index_gl_type: int
643
- index_c_type: CTypesDataType
644
- index_element_size: int
645
- index_buffer: IndexedBufferObject
646
- _initial_index_count = 16
647
- _vertex_class = IndexedVertexList
675
+ def _create_vao(self) -> None:
676
+ """Handled by buckets."""
648
677
 
649
- def __init__(self, attribute_meta: dict[str, dict[str, Any]], # noqa: D107
650
- index_gl_type: int = GL_UNSIGNED_INT) -> None:
651
- super().__init__(attribute_meta)
678
+ def bind_vao(self):
679
+ self.vertex_buffers.commit()
652
680
 
653
- self.index_allocator = allocation.Allocator(self._initial_index_count)
681
+ def alloc_to_group(self, vertex_list, group):
682
+ super().alloc_to_group(vertex_list, group)
683
+ key = (vertex_list.start, vertex_list.count)
684
+ self._instance_map[key] = vertex_list.instance_bucket
654
685
 
655
- self.index_gl_type = index_gl_type
656
- self.index_c_type = shader._c_types[index_gl_type] # noqa: SLF001
657
- self.index_element_size = ctypes.sizeof(self.index_c_type)
658
- self.index_buffer = IndexedBufferObject(self.index_allocator.capacity * self.index_element_size,
659
- shader._c_types[index_gl_type],
660
- self.index_element_size,
661
- 1)
686
+ def create(self, group: Group, count: int, indices: Sequence[int] | None = None) -> VertexListBase: # noqa: ARG002
687
+ start = self.safe_alloc(count)
688
+ bucket = self.instance_domain.get_arrays_bucket(mode=0, first_vertex=start, vertex_count=count)
689
+ vlist = self._vertexlist_class(self, group, start, count, bucket)
690
+ self.alloc_to_group(vlist, group)
691
+ return vlist
692
+
693
+ class InstancedIndexedVertexDomainBase(IndexedVertexDomainBase):
694
+ def __init__(self, context: SurfaceContext, initial_count: int, attribute_meta: dict[str, Attribute],
695
+ index_type: DataTypes = "I") -> None:
696
+ super().__init__(context, initial_count, attribute_meta, index_type)
697
+ self.instance_domain = self.create_instance_domain(initial_count)
698
+ self._instance_map = {}
699
+
700
+ @abstractmethod
701
+ def create_instance_domain(self, size: int) -> BaseInstanceDomain:
702
+ ...
703
+
704
+ def _create_vao(self) -> None:
705
+ """Handled by buckets."""
706
+
707
+ def bind_vao(self):
708
+ # VAO's are actually bound when instance buckets are drawn, but we can update the shared buffers atleast.
709
+ self.vertex_buffers.commit()
710
+ self.index_stream.commit()
711
+
712
+ def alloc_to_group(self, vertex_list, group):
713
+ super().alloc_to_group(vertex_list, group)
714
+ key = (vertex_list.index_start, vertex_list.index_count)
715
+ self._instance_map[key] = vertex_list.instance_bucket
716
+
717
+ def create(self, group: Group, count: int, indices: Sequence[int] | None) -> InstanceIndexedVertexListBase:
718
+ """Create an :py:class:`IndexedVertexList` in this domain.
662
719
 
663
- self.vao.bind()
664
- self.index_buffer.bind_to_index_buffer()
665
- self.vao.unbind()
720
+ Args:
721
+ group:
722
+ The :py:class:`Group` the resulting vertex list will be drawn with.
723
+ count:
724
+ Number of vertices to create
725
+ indices:
726
+ Indices used for this vertex list.
666
727
 
728
+ """
729
+ index_count = len(indices)
730
+ start = self.safe_alloc(count)
731
+ index_start = self.safe_index_alloc(index_count)
732
+ base_vertex = start if self._supports_base_vertex else 0
733
+ bucket = self.instance_domain.get_elements_bucket(
734
+ mode=0, # Separate Mode from draw call into bucket at some point?
735
+ first_index=index_start,
736
+ index_count=index_count,
737
+ index_type=self.index_type,
738
+ base_vertex=base_vertex,
739
+ )
740
+ vertex_list = self._vertexlist_class(self, group, start, count, index_start, index_count, self.index_type, base_vertex, bucket)
741
+ vertex_list.indices = indices
742
+ self.alloc_to_group(vertex_list, group)
743
+ return vertex_list
744
+
745
+ def _create_vertex_class(self) -> type:
746
+ mixin = _LocalIndexSupport if self._supports_base_vertex else _RunningIndexSupport
667
747
  # Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
668
- self._vertexlist_class = type(self._vertex_class.__name__, (self._vertex_class,),
669
- self._property_dict)
748
+ return type(self._vertex_class.__name__, (mixin, self._vertex_class),
749
+ self.vertex_buffers._property_dict) # noqa: SLF001
670
750
 
671
- def safe_index_alloc(self, count: int) -> int:
672
- """Allocate indices, resizing the buffers if necessary."""
751
+ class BaseStream(ABC):
752
+ """A container that handles a set of buffers to be used with domains."""
753
+ def __init__(self, size: int) -> None:
754
+ """Initialize the stream and create an allocator.
755
+
756
+ Args:
757
+ size: Initial allocator and buffer size.
758
+ """
759
+ self._capacity = size
760
+ self.allocator = allocation.Allocator(size)
761
+ self.buffers = []
762
+
763
+ def commit(self) -> None:
764
+ """Binds buffers and commits all pending data to the graphics API."""
765
+ for buf in self.buffers:
766
+ buf.commit()
767
+
768
+ @abstractmethod
769
+ def bind_into(self, vao) -> None:
770
+ """Record this stream into the VAO.
771
+
772
+ The VAO should be bound before this function is called.
773
+ """
774
+
775
+ def alloc(self, count: int) -> int:
776
+ """Allocate a region of data, resizing the buffers if necessary."""
673
777
  try:
674
- return self.index_allocator.alloc(count)
778
+ return self.allocator.alloc(count)
675
779
  except allocation.AllocatorMemoryException as e:
676
780
  capacity = _nearest_pow2(e.requested_capacity)
677
- self.index_buffer.resize(capacity * self.index_element_size)
678
- self.index_allocator.set_capacity(capacity)
679
- return self.index_allocator.alloc(count)
781
+ self.resize(capacity)
782
+ return self.allocator.alloc(count)
680
783
 
681
- def safe_index_realloc(self, start: int, count: int, new_count: int) -> int:
682
- """Reallocate indices, resizing the buffers if necessary."""
784
+ def resize(self, capacity: int) -> None:
785
+ """Resize all buffers to the specified capacity.
786
+
787
+ Size is passed as capacity * stride.
788
+ """
789
+ if capacity <= self.allocator.capacity:
790
+ return
791
+ self.allocator.set_capacity(capacity)
792
+ for buf in self.buffers:
793
+ buf.resize(capacity * buf.stride)
794
+
795
+ def dealloc(self, start: int, count: int) -> None:
796
+ self.allocator.dealloc(start, count)
797
+
798
+ def realloc(self, start: int, count: int, new_count: int) -> int:
799
+ """Reallocate a region of data, resizing the buffers if necessary."""
683
800
  try:
684
- return self.index_allocator.realloc(start, count, new_count)
801
+ return self.allocator.realloc(start, count, new_count)
685
802
  except allocation.AllocatorMemoryException as e:
686
803
  capacity = _nearest_pow2(e.requested_capacity)
687
- self.index_buffer.resize(capacity * self.index_element_size)
688
- self.index_allocator.set_capacity(capacity)
689
- return self.index_allocator.realloc(start, count, new_count)
804
+ self.resize(capacity)
805
+ return self.allocator.realloc(start, count, new_count)
806
+ @abstractmethod
807
+ def set_region(self, start: int, count: int, data) -> None: ...
690
808
 
691
- def create(self, count: int, index_count: int) -> IndexedVertexList:
692
- """Create an :py:class:`IndexedVertexList` in this domain.
693
809
 
694
- Args:
695
- count:
696
- Number of vertices to create
697
- index_count:
698
- Number of indices to create
810
+ class VertexStream(BaseStream):
811
+ """A stream of buffers to be used with per-vertex attributes."""
812
+ attrib_name_buffers: dict[str, AttributeBufferObject]
813
+ attribute_meta: Sequence[Attribute]
814
+ def __init__(self, ctx: SurfaceContext, initial_size: int, attrs: Sequence[Attribute], *, divisor: int = 0):
815
+ super().__init__(initial_size)
816
+ self._ctx = ctx
817
+ self.attribute_names = {} # name: attribute
818
+ self.buffers = []
819
+ self.attrib_name_buffers = {} # dict of AttributeName: AttributeBufferObject (for VertexLists)
699
820
 
700
- """
701
- start = self.safe_alloc(count)
702
- index_start = self.safe_index_alloc(index_count)
703
- return self._vertexlist_class(self, start, count, index_start, index_count)
821
+ self._property_dict = {}
822
+ self.attribute_meta = attrs
823
+ self._allocate_buffers()
824
+
825
+ def get_buffer(self, size, attribute):
826
+ raise NotImplementedError
827
+
828
+ def get_graphics_attribute(self, attribute: Attribute, view: AttributeView) -> GraphicsAttribute:
829
+ raise NotImplementedError
830
+
831
+ def _create_separate_buffers(self, attributes: Sequence[Attribute]) -> None:
832
+ """Takes the attributes and creates a separate buffer for each attribute."""
833
+ for attribute in attributes:
834
+ name = attribute.fmt.name
835
+
836
+ stride = attribute.fmt.components * attribute.element_size
837
+ view = AttributeView(offset=0, stride=stride)
838
+ self.attribute_names[name] = attribute = self.get_graphics_attribute(attribute, view)
839
+
840
+ self.attrib_name_buffers[name] = buffer = self.get_buffer(stride * self.allocator.capacity, attribute)
841
+
842
+ self.buffers.append(buffer)
843
+
844
+ # Create custom property to be used in the VertexListBase:
845
+ self._property_dict[name] = _make_attribute_property(name)
846
+
847
+ def _create_interleaved_buffers(self) -> NoReturn:
848
+ """Creates a single buffer for all passed attributes."""
849
+ raise NotImplementedError
850
+
851
+ def _allocate_buffers(self) -> None:
852
+ for attrib in self.attribute_meta:
853
+ fmt_dt = attrib.fmt.data_type
854
+ assert fmt_dt in DataTypeTuple, f"'{fmt_dt}' is not a valid attribute format for '{attrib.fmt.name}'."
855
+
856
+ # Only support separate buffers per attrib currently.
857
+ self._create_separate_buffers(self.attribute_meta)
858
+
859
+ def set_region(self, start: int, count: int, data_by_attr: dict[str, Any]):
860
+ for name, buf in self.attrib_name_buffers.items():
861
+ buf.set_region(start, count, data_by_attr[name])
862
+
863
+ def set_attribute_region(self, name: str, start: int, count: int, data: Any):
864
+ buf = self.attrib_name_buffers[name]
865
+ return buf.set_region(start, count)
866
+
867
+ def get_attribute_region(self, name: str, start: int, count: int):
868
+ buf = self.attrib_name_buffers[name]
869
+ return buf.get_region(start, count)
870
+
871
+ def invalidate_attribute_region(self, name: str, start: int, count: int):
872
+ buf = self.attrib_name_buffers[name]
873
+ buf.invalidate_region(start, count)
874
+
875
+ def copy_data(
876
+ self,
877
+ dst_slot: int,
878
+ dst_stream: VertexStream | InstanceStream,
879
+ src_slot: int,
880
+ count: int = 1,
881
+ attrs: Iterable[str] | None = None,
882
+ *,
883
+ strict: bool = False,
884
+ ) -> None:
885
+ if attrs is None:
886
+ dst_names = set(dst_stream.attrib_name_buffers.keys())
887
+ src_names = set(self.attrib_name_buffers.keys())
888
+ names = dst_names & src_names
889
+ if strict and dst_names != src_names:
890
+ err = (f"Attribute layout mismatch: missing in dst={sorted(src_names - dst_names)}, "
891
+ f"missing in src={sorted(dst_names - src_names)}")
892
+ raise ValueError(err)
893
+ else:
894
+ names = [n for n in attrs if n in self.attrib_name_buffers and n in dst_stream.attrib_name_buffers]
895
+ if strict and len(names) != len(list(attrs)):
896
+ err = f"Requested attribute not present in both streams. {names}, {attrs}"
897
+ raise ValueError(err)
704
898
 
705
- def draw(self, mode: int) -> None:
706
- """Draw all vertices in the domain.
899
+ for name in names:
900
+ dst_buf = dst_stream.attrib_name_buffers[name]
901
+ src_buf = self.attrib_name_buffers[name]
707
902
 
708
- All vertices in the domain are drawn at once. This is the
709
- most efficient way to render primitives.
903
+ data = src_buf.get_region(src_slot, count)
904
+ dst_buf.set_region(dst_slot, count, data)
710
905
 
711
- Args:
712
- mode:
713
- OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
906
+ def __repr__(self):
907
+ return f'{self.__class__.__name__}(attributes={list(self.attribute_meta)}, alloc={self.allocator})'
714
908
 
715
- """
716
- self.vao.bind()
717
- for buffer, _ in self.buffer_attributes:
718
- buffer.commit()
719
-
720
- self.index_buffer.commit()
721
-
722
- starts, sizes = self.index_allocator.get_allocated_regions()
723
- primcount = len(starts)
724
- if primcount == 0:
725
- pass
726
- elif primcount == 1:
727
- # Common case
728
- glDrawElements(mode, sizes[0], self.index_gl_type,
729
- self.index_buffer.ptr + starts[0] * self.index_element_size)
730
- else:
731
- starts = [s * self.index_element_size + self.index_buffer.ptr for s in starts]
732
- starts = (ctypes.POINTER(GLvoid) * primcount)(*(GLintptr * primcount)(*starts))
733
- sizes = (GLsizei * primcount)(*sizes)
734
- glMultiDrawElements(mode, sizes, self.index_gl_type, starts, primcount)
909
+ class InstanceStream(VertexStream):
910
+ """Handles a stream of buffers to be used with the per-instance attributes."""
735
911
 
736
- def draw_subset(self, mode: int, vertex_list: IndexedVertexList) -> None:
737
- """Draw a specific IndexedVertexList in the domain.
912
+ class IndexStream(BaseStream):
913
+ """A container to manage an index buffer for a domain."""
738
914
 
739
- The `vertex_list` parameter specifies a :py:class:`IndexedVertexList`
740
- to draw. Only primitives in that list will be drawn.
915
+ def __init__(self, ctx, data_type: DataTypes, initial_elems: int):
916
+ super().__init__(initial_elems)
917
+ self.ctx = ctx
918
+ self.data_type = data_type
919
+ self.buffer = self._create_buffer()
920
+ self.buffers = [self.buffer]
741
921
 
742
- Args:
743
- mode:
744
- OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
745
- vertex_list:
746
- Vertex list to draw.
747
- """
748
- self.vao.bind()
749
- for buffer, _ in self.buffer_attributes:
750
- buffer.commit()
922
+ def _create_buffer(self) -> IndexedBufferObject:
923
+ raise NotImplementedError
751
924
 
752
- self.index_buffer.commit()
925
+ def commit(self) -> None:
926
+ self.buffer.commit()
753
927
 
754
- glDrawElements(mode, vertex_list.index_count, self.index_gl_type,
755
- self.index_buffer.ptr +
756
- vertex_list.index_start * self.index_element_size)
928
+ def get_region(self, start: int, count: int) -> Any:
929
+ return self.buffer.get_region(start, count)
757
930
 
931
+ def bind_into(self, vao) -> None:
932
+ self.buffer.bind_to_index_buffer()
758
933
 
759
- class InstancedIndexedVertexDomain(IndexedVertexDomain, InstancedVertexDomain):
760
- """Management of a set of indexed vertex lists.
934
+ def set_region(self, start: int, count: int, data) -> None:
935
+ self.buffer.set_region(start, count, data)
761
936
 
762
- Construction of an indexed vertex domain is usually done with the
763
- :py:func:`create_domain` function.
937
+ def copy_region(self, dst: int, src: int, count: int) -> None:
938
+ self.buffer.copy_region(dst, src, count)
939
+
940
+
941
+ class VertexArrayProtocol(Protocol):
942
+ def bind(self): ...
943
+ def unbind(self): ...
944
+
945
+
946
+ class VertexArrayBinding:
947
+ """A wrapper for a Vertex Array Object that binds streams.
948
+
949
+ VAO's store which attribute layouts are used, as well as which buffer object each attribute pulls from.
950
+
951
+ In the case of instanced drawing, each instance needs its own VAO as their per-instance data are separate buffers.
764
952
  """
765
- _initial_index_count: int = 16
953
+ streams: list[VertexStream | InstanceStream | IndexStream]
766
954
 
767
- def __init__(self, attribute_meta: dict[str, dict[str, Any]], # noqa: D107
768
- index_gl_type: int = GL_UNSIGNED_INT) -> None:
769
- super().__init__(attribute_meta, index_gl_type)
955
+ def __init__(self, ctx: SurfaceContext, streams: list[VertexStream | InstanceStream | IndexStream]):
956
+ # attr_map: semantic/name -> location (from ShaderProgram inspection)
957
+ self._ctx = ctx
958
+ self.vao = self._create_vao()
959
+ self.streams = streams
960
+ self._link()
770
961
 
771
- def safe_index_alloc(self, count: int) -> int:
772
- """Allocate indices, resizing the buffers if necessary.
962
+ def bind(self):
963
+ raise NotImplementedError
773
964
 
774
- Returns:
775
- The starting index of the allocated region.
776
- """
777
- try:
778
- return self.index_allocator.alloc(count)
779
- except allocation.AllocatorMemoryException as e:
780
- capacity = _nearest_pow2(e.requested_capacity)
781
- self.index_buffer.resize(capacity * self.index_element_size)
782
- self.index_allocator.set_capacity(capacity)
783
- return self.index_allocator.alloc(count)
965
+ def _create_vao(self) -> VertexArrayProtocol: ...
784
966
 
785
- def safe_index_realloc(self, start: int, count: int, new_count: int) -> int:
786
- """Reallocate indices, resizing the buffers if necessary."""
787
- try:
788
- return self.index_allocator.realloc(start, count, new_count)
789
- except allocation.AllocatorMemoryException as e:
790
- capacity = _nearest_pow2(e.requested_capacity)
791
- self.index_buffer.resize(capacity * self.index_element_size)
792
- self.index_allocator.set_capacity(capacity)
793
- return self.index_allocator.realloc(start, count, new_count)
967
+ def _link(self):
968
+ """Link the all streams to the VAO."""
794
969
 
795
- def create(self, count: int, index_count: int) -> IndexedVertexList:
796
- """Create an :py:class:`IndexedVertexList` in this domain.
970
+ def __repr__(self):
971
+ return f'<{self.__class__.__name__}@{id(self):x} vao={self.vao}, streams={self.streams}>'
797
972
 
798
- Args:
799
- count:
800
- Number of vertices to create
801
- index_count:
802
- Number of indices to create
803
973
 
804
- """
805
- start = self.safe_alloc(count)
806
- index_start = self.safe_index_alloc(index_count)
807
- return self._vertexlist_class(self, start, count, index_start, index_count)
808
974
 
809
- def draw(self, mode: int) -> None:
810
- """Draw all vertices in the domain.
975
+ class VertexGroupBucket(allocation.RangeAllocator):
976
+ """A grouping of vertex lists belonging to a single group in a domain.
811
977
 
812
- All vertices in the domain are drawn at once. This is the
813
- most efficient way to render primitives.
978
+ Vertex lists are still owned by the domain, but this allows states to be rendered together if possible.
979
+ """
814
980
 
815
- Args:
816
- mode:
817
- OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
981
+ __slots__ = ("_merged", "_ranges", "is_dirty")
818
982
 
819
- """
820
- self.vao.bind()
821
- for buffer, _ in self.buffer_attributes:
822
- buffer.commit()
983
+ def __init__(self) -> None:
984
+ super().__init__()
823
985
 
824
- starts, sizes = self.index_allocator.get_allocated_regions()
825
- glDrawElementsInstanced(mode, sizes[0], self.index_gl_type,
826
- self.index_buffer.ptr + starts[0] * self.index_element_size, self._instances)
986
+ def add_vertex_list(self, vl: VertexListBase) -> None:
987
+ self.add(vl.start, vl.count)
827
988
 
828
- def draw_subset(self, mode: int, vertex_list: IndexedVertexList) -> None:
829
- """Draw a specific IndexedVertexList in the domain.
989
+ def remove_vertex_list(self, vl: VertexListBase) -> None:
990
+ self.remove(vl.start, vl.count)
830
991
 
831
- The ``vertex_list`` parameter specifies a :py:class:`IndexedVertexList`
832
- to draw. Only primitives in that list will be drawn.
833
992
 
834
- Args:
835
- mode:
836
- OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
837
- vertex_list:
838
- Vertex list to draw.
993
+ class IndexedVertexGroupBucket(allocation.RangeAllocator):
994
+ """A grouping of indexed vertex lists belonging to a single group in a domain.
839
995
 
840
- """
841
- self.vao.bind()
842
- for buffer, _ in self.buffer_attributes:
843
- buffer.commit()
996
+ Vertex lists are still owned by the domain, but this allows states to be rendered together if possible.
997
+ """
998
+ __slots__ = ("_merged", "_ranges", "is_dirty")
999
+
1000
+ def add_vertex_list(self, vl: IndexedVertexListBase) -> None:
1001
+ self.add(vl.index_start, vl.index_count)
844
1002
 
845
- glDrawElementsInstanced(mode, vertex_list.index_count, self.index_gl_type,
846
- self.index_buffer.ptr +
847
- vertex_list.index_start * self.index_element_size, self._instances)
1003
+ def remove_vertex_list(self, vl: IndexedVertexListBase) -> None:
1004
+ self.remove(vl.index_start, vl.index_count)