gl46 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gl46/__init__.py ADDED
@@ -0,0 +1,99 @@
1
+ """OpenGL 4.6 Direct State Access utility library (gl46)."""
2
+
3
+ from gl46.barrier import Barrier, MemoryBarrier
4
+ from gl46.base import GLObject
5
+ from gl46.buffer import EBO, IBO, Buffer, IndirectDrawBuffer, ShaderStorage
6
+ from gl46.camera import Camera
7
+ from gl46.debug import DebugOutput
8
+ from gl46.errors import (
9
+ GL46Error,
10
+ GLCompilationError,
11
+ GLFileNotFoundError,
12
+ GLLinkError,
13
+ GLObjectDeletedError,
14
+ GLVersionError,
15
+ )
16
+ from gl46.framebuffer import Framebuffer
17
+ from gl46.gl import GL, PyOpenGLGL, configure, get_gl
18
+ from gl46.material import Material
19
+ from gl46.pipeline import ProgramPipeline
20
+ from gl46.program import Program
21
+ from gl46.query import Query
22
+ from gl46.renderbuffer import Renderbuffer, RenderbufferMultisample
23
+ from gl46.sampler import Sampler
24
+ from gl46.shader import Shader, ShaderStage
25
+ from gl46.texture import (
26
+ Texture2D,
27
+ Texture2DArray,
28
+ TextureBuffer,
29
+ TextureCubeMapArray,
30
+ TextureMultisample,
31
+ )
32
+ from gl46.transformfeedback import TransformFeedback
33
+ from gl46.version import parse_version, require_core, require_dsa
34
+ from gl46.vertexarray import VertexArray, VertexAttrib
35
+ from gl46.window import Window
36
+
37
+ GLenum = GLuint = GLint = int
38
+
39
+ __all__ = [
40
+ "EBO",
41
+ "GL",
42
+ "IBO",
43
+ "Barrier",
44
+ "Buffer",
45
+ "Camera",
46
+ "DebugOutput",
47
+ "Framebuffer",
48
+ "GL46Error",
49
+ "GLCompilationError",
50
+ "GLFileNotFoundError",
51
+ "GLLinkError",
52
+ "GLObject",
53
+ "GLObjectDeletedError",
54
+ "GLVersionError",
55
+ "GLenum",
56
+ "GLint",
57
+ "GLuint",
58
+ "IndirectDrawBuffer",
59
+ "Material",
60
+ "MemoryBarrier",
61
+ "Program",
62
+ "ProgramPipeline",
63
+ "PyOpenGLGL",
64
+ "Query",
65
+ "Renderbuffer",
66
+ "RenderbufferMultisample",
67
+ "Sampler",
68
+ "Shader",
69
+ "ShaderStage",
70
+ "ShaderStorage",
71
+ "Texture2D",
72
+ "Texture2DArray",
73
+ "TextureBuffer",
74
+ "TextureCubeMapArray",
75
+ "TextureMultisample",
76
+ "TransformFeedback",
77
+ "VertexArray",
78
+ "VertexAttrib",
79
+ "Window",
80
+ "configure",
81
+ "get_gl",
82
+ "parse_version",
83
+ "require_core",
84
+ "require_dsa",
85
+ ]
86
+
87
+ try:
88
+ from gl46.window_imgui import build_imgui_params, run_imgui_app
89
+ except ImportError: # imgui-bundle missing or partially broken
90
+ pass
91
+ else:
92
+ __all__ += ["build_imgui_params", "run_imgui_app"]
93
+
94
+ try:
95
+ from gl46.window_qt import GL46Widget, make_qt_format, run_qt_app
96
+ except ImportError: # PySide6 not installed
97
+ pass
98
+ else:
99
+ __all__ += ["GL46Widget", "make_qt_format", "run_qt_app"]
gl46/barrier.py ADDED
@@ -0,0 +1,82 @@
1
+ """High-level GPU memory barrier helpers around :func:`glMemoryBarrier`.
2
+
3
+ Compute shader writes to storage buffers, images and atomic counters are
4
+ not ordered against later reads unless an explicit barrier is posted.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from gl46.constants import (
10
+ GL_ALL_BARRIER_BITS,
11
+ GL_ATOMIC_COUNTER_BARRIER_BIT,
12
+ GL_BUFFER_UPDATE_BARRIER_BIT,
13
+ GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT,
14
+ GL_COMMAND_BARRIER_BIT,
15
+ GL_ELEMENT_ARRAY_BARRIER_BIT,
16
+ GL_FRAMEBUFFER_BARRIER_BIT,
17
+ GL_PIXEL_BUFFER_BARRIER_BIT,
18
+ GL_QUERY_BUFFER_BARRIER_BIT,
19
+ GL_SHADER_IMAGE_ACCESS_BARRIER_BIT,
20
+ GL_SHADER_STORAGE_BARRIER_BIT,
21
+ GL_TEXTURE_FETCH_BARRIER_BIT,
22
+ GL_TRANSFORM_FEEDBACK_BARRIER_BIT,
23
+ GL_UNIFORM_BARRIER_BIT,
24
+ GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT,
25
+ )
26
+ from gl46.gl import GL, get_gl
27
+
28
+
29
+ class Barrier:
30
+ """Named memory-barrier bits plus helpers to post the common combos.
31
+
32
+ The class attributes are the individual ``GL_*_BARRIER_BIT`` masks; the
33
+ helpers post the barrier that pairs well with a freshly-written
34
+ resource. Callers may also ``OR`` several attributes together and post
35
+ them with :meth:`post`.
36
+ """
37
+
38
+ SHADER_STORAGE = GL_SHADER_STORAGE_BARRIER_BIT
39
+ SHADER_IMAGE_ACCESS = GL_SHADER_IMAGE_ACCESS_BARRIER_BIT
40
+ TEXTURE_FETCH = GL_TEXTURE_FETCH_BARRIER_BIT
41
+ VERTEX_ATTRIB_ARRAY = GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT
42
+ ELEMENT_ARRAY = GL_ELEMENT_ARRAY_BARRIER_BIT
43
+ UNIFORM = GL_UNIFORM_BARRIER_BIT
44
+ COMMAND = GL_COMMAND_BARRIER_BIT
45
+ PIXEL_BUFFER = GL_PIXEL_BUFFER_BARRIER_BIT
46
+ BUFFER_UPDATE = GL_BUFFER_UPDATE_BARRIER_BIT
47
+ FRAMEBUFFER = GL_FRAMEBUFFER_BARRIER_BIT
48
+ TRANSFORM_FEEDBACK = GL_TRANSFORM_FEEDBACK_BARRIER_BIT
49
+ ATOMIC_COUNTER = GL_ATOMIC_COUNTER_BARRIER_BIT
50
+ QUERY_BUFFER = GL_QUERY_BUFFER_BARRIER_BIT
51
+ CLIENT_MAPPED = GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT
52
+ ALL = GL_ALL_BARRIER_BITS
53
+
54
+ @staticmethod
55
+ def post(barriers: int, gl: GL | None = None) -> None:
56
+ """Post ``glMemoryBarrier`` with the given barrier bits."""
57
+ (gl if gl is not None else get_gl()).memory_barrier(barriers)
58
+
59
+ @classmethod
60
+ def shader_storage(cls, gl: GL | None = None) -> None:
61
+ """Post a shader-storage barrier before reading an SSBO written earlier."""
62
+ cls.post(cls.SHADER_STORAGE, gl)
63
+
64
+ @classmethod
65
+ def image_to_texture(cls, gl: GL | None = None) -> None:
66
+ """Post a barrier for compute writes to images read back as textures."""
67
+ cls.post(cls.SHADER_IMAGE_ACCESS | cls.TEXTURE_FETCH, gl)
68
+
69
+ @classmethod
70
+ def vertex_attributes(cls, gl: GL | None = None) -> None:
71
+ """Post a barrier after buffer writes read as vertex attributes."""
72
+ cls.post(cls.VERTEX_ATTRIB_ARRAY, gl)
73
+
74
+ @classmethod
75
+ def atomic_to_storage(cls, gl: GL | None = None) -> None:
76
+ """Post a barrier between atomic-counter and SSBO access."""
77
+ cls.post(cls.ATOMIC_COUNTER | cls.SHADER_STORAGE, gl)
78
+
79
+
80
+ MemoryBarrier = Barrier
81
+
82
+ __all__ = ["Barrier", "MemoryBarrier"]
gl46/base.py ADDED
@@ -0,0 +1,53 @@
1
+ """Base class shared by every GPU-backed object in :mod:`gl46`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from gl46.errors import GLObjectDeletedError
6
+ from gl46.gl import GL, get_gl
7
+
8
+
9
+ class GLObject:
10
+ """A GPU resource that owns a GL handle.
11
+
12
+ Subclasses allocate the handle in their ``__init__`` and implement
13
+ :meth:`_delete_impl` to release it. Calling :meth:`delete` twice, or
14
+ using a deleted object, is a no-op / guarded error respectively.
15
+ """
16
+
17
+ _gl: GL
18
+ _id: int
19
+ _deleted: bool
20
+
21
+ def __init__(self, gl: GL | None = None) -> None:
22
+ self._gl = gl if gl is not None else get_gl()
23
+ self._id = -1
24
+ self._deleted = False
25
+
26
+ @property
27
+ def id(self) -> int:
28
+ """The raw GL name (handle) of this object."""
29
+ self._require_alive()
30
+ return self._id
31
+
32
+ @property
33
+ def deleted(self) -> bool:
34
+ """True once the underlying GL object has been released."""
35
+ return self._deleted
36
+
37
+ def _require_alive(self) -> None:
38
+ if self._deleted:
39
+ raise GLObjectDeletedError(f"{type(self).__name__} has been deleted")
40
+
41
+ def delete(self) -> None:
42
+ """Release the GL resource. Safe to call multiple times."""
43
+ if self._deleted:
44
+ return
45
+ self._require_alive()
46
+ self._delete_impl()
47
+ self._deleted = True
48
+
49
+ def _delete_impl(self) -> None:
50
+ raise NotImplementedError
51
+
52
+
53
+ __all__ = ["GLObject"]
gl46/buffer.py ADDED
@@ -0,0 +1,351 @@
1
+ """DSA vertex/index buffer wrapper around :func:`glCreateBuffers`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ from numpy.typing import NDArray
10
+
11
+ from gl46.base import GLObject
12
+ from gl46.constants import (
13
+ GL_ARRAY_BUFFER,
14
+ GL_DRAW_INDIRECT_BUFFER,
15
+ GL_DYNAMIC_STORAGE_BIT,
16
+ GL_ELEMENT_ARRAY_BUFFER,
17
+ GL_MAP_COHERENT_BIT,
18
+ GL_MAP_PERSISTENT_BIT,
19
+ GL_MAP_WRITE_BIT,
20
+ GL_SHADER_STORAGE_BUFFER,
21
+ )
22
+ from gl46.gl import GL, DataPayload
23
+
24
+
25
+ def _payload_size(payload: DataPayload) -> int:
26
+ return len(payload) if isinstance(payload, bytes) else int(payload.nbytes)
27
+
28
+
29
+ class Buffer(GLObject):
30
+ """An immutable-size GPU buffer (VBO/IBO/UBO/SSBO).
31
+
32
+ Storage is allocated once via ``glNamedBufferStorage``; its size and
33
+ usage flags are fixed for the lifetime of the buffer, which is exactly
34
+ the DSA idiom promoted by the OpenGL 4.6 style.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ size: int,
40
+ *,
41
+ dynamic: bool = False,
42
+ flags: int = 0,
43
+ gl: GL | None = None,
44
+ initial_data: DataPayload | None = None,
45
+ ) -> None:
46
+ super().__init__(gl)
47
+ if size < 0:
48
+ raise ValueError(f"buffer size must be >= 0, got {size}")
49
+ if initial_data is not None:
50
+ data_size = _payload_size(initial_data)
51
+ if data_size != size:
52
+ raise ValueError(
53
+ f"initial_data is {data_size} bytes but buffer size is {size} "
54
+ f"(glNamedBufferStorage copies exactly {size} bytes from the "
55
+ f"payload, so the payload must match the storage size)"
56
+ )
57
+ self._size = size
58
+ if dynamic:
59
+ flags |= GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT
60
+ self._flags = flags
61
+ (self._id,) = self._gl.create_buffers(1)
62
+ self._gl.named_buffer_storage(self._id, size, initial_data, self._flags)
63
+
64
+ @classmethod
65
+ def from_data(
66
+ cls,
67
+ data: DataPayload,
68
+ *,
69
+ dynamic: bool = False,
70
+ flags: int = 0,
71
+ gl: GL | None = None,
72
+ ) -> Buffer:
73
+ """Allocate a buffer sized to ``data`` and upload it immediately.
74
+
75
+ The data is provided to ``glNamedBufferStorage`` directly, which is
76
+ the only way to populate immutable (non-``GL_DYNAMIC_STORAGE_BIT``)
77
+ storage; ``glNamedBufferSubData`` would raise ``GL_INVALID_OPERATION``.
78
+ """
79
+ return cls(_payload_size(data), dynamic=dynamic, flags=flags, gl=gl, initial_data=data)
80
+
81
+ @classmethod
82
+ def persistent(
83
+ cls,
84
+ size: int,
85
+ *,
86
+ initial_data: DataPayload | None = None,
87
+ gl: GL | None = None,
88
+ ) -> Buffer:
89
+ """Allocate ``size`` bytes persistently mapped and CPU/GPU coherent.
90
+
91
+ The storage gets ``GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT |
92
+ GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT``, which allows the CPU
93
+ to write straight into the mapped region while the GPU reads it,
94
+ without any explicit flush or unmap. ``initial_data`` may seed the
95
+ storage at allocation time.
96
+ """
97
+ flags = (
98
+ GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT
99
+ )
100
+ return cls(size, flags=flags, gl=gl, initial_data=initial_data)
101
+
102
+ @property
103
+ def size_bytes(self) -> int:
104
+ return self._size
105
+
106
+ @property
107
+ def storage_flags(self) -> int:
108
+ return self._flags
109
+
110
+ def update(self, data: DataPayload, *, offset: int = 0) -> None:
111
+ """Upload ``data`` into the buffer at ``offset`` (``glNamedBufferSubData``)."""
112
+ self._require_alive()
113
+ size = _payload_size(data)
114
+ if offset + size > self._size:
115
+ raise ValueError(
116
+ f"upload of {size} bytes at offset {offset} "
117
+ f"exceeds buffer capacity of {self._size} bytes"
118
+ )
119
+ self._gl.named_buffer_sub_data(self._id, offset, size, data)
120
+
121
+ def bind(self, target: int = GL_ARRAY_BUFFER) -> None:
122
+ """Bind this buffer to ``target`` (usually only needed for legacy paths)."""
123
+ self._require_alive()
124
+ self._gl.bind_buffer(target, self._id)
125
+
126
+ def unbind(self, target: int = GL_ARRAY_BUFFER) -> None:
127
+ """Unbind any buffer from ``target`` (binds the null buffer)."""
128
+ self._gl.bind_buffer(target, 0)
129
+
130
+ def bind_range(
131
+ self,
132
+ target: int,
133
+ index: int,
134
+ *,
135
+ offset: int = 0,
136
+ size: int | None = None,
137
+ ) -> None:
138
+ """Bind a range of this buffer to a indexed target (``glBindBufferRange``).
139
+
140
+ ``size=None`` binds from ``offset`` to the end of the buffer; a
141
+ ``size`` of 0 binds an empty range. Raises :class:`ValueError` if
142
+ the window is out of bounds.
143
+ """
144
+ self._require_alive()
145
+ effective = self._size - offset if size is None else size
146
+ if offset < 0 or effective < 0 or offset + effective > self._size:
147
+ raise ValueError(
148
+ f"bind_range({offset}, {effective}) exceeds buffer capacity of {self._size} bytes"
149
+ )
150
+ self._gl.bind_buffer_range(target, index, self._id, offset, effective)
151
+
152
+ def bind_base(self, target: int, index: int) -> None:
153
+ """Bind the whole buffer to an indexed target (``glBindBufferBase``)."""
154
+ self._require_alive()
155
+ self._gl.bind_buffer_base(target, index, self._id)
156
+
157
+ def map(self, access: int = GL_MAP_WRITE_BIT) -> MappedBuffer:
158
+ """Map the whole buffer for direct CPU access (``glMapNamedBuffer``).
159
+
160
+ Returns a :class:`MappedBuffer` context manager; the mapping stays
161
+ valid until it is used as a ``with`` block body ends (or
162
+ :meth:`Buffer.unmap` is called). Only valid when the storage flags
163
+ include ``GL_MAP_*`` (and ``GL_MAP_PERSISTENT_BIT`` for a persistent
164
+ mapping).
165
+ """
166
+ self._require_alive()
167
+ address = self._gl.map_named_buffer(self._id, access)
168
+ return MappedBuffer(self, address, 0, self._size, access=access)
169
+
170
+ def map_range(self, offset: int, length: int, access: int = GL_MAP_WRITE_BIT) -> MappedBuffer:
171
+ """Map ``length`` bytes at ``offset`` (``glMapNamedBufferRange``).
172
+
173
+ Returns a :class:`MappedBuffer` context manager covering the mapped
174
+ window; GL writes start at ``offset`` but ``as_numpy`` offsets are
175
+ relative to the window start. ``length`` must be at least 1 byte.
176
+ """
177
+ self._require_alive()
178
+ if length <= 0:
179
+ raise ValueError(f"map_range length must be > 0, got {length}")
180
+ if offset < 0 or offset + length > self._size:
181
+ raise ValueError(
182
+ f"map_range({offset}, {length}) exceeds buffer capacity of {self._size} bytes"
183
+ )
184
+ address = self._gl.map_named_buffer_range(self._id, offset, length, access)
185
+ return MappedBuffer(self, address, offset, length, access=access)
186
+
187
+ def unmap(self) -> None:
188
+ """Release the mapping created by :meth:`map` or :meth:`map_range`."""
189
+ self._require_alive()
190
+ self._gl.unmap_named_buffer(self._id)
191
+
192
+ def flush_mapped(self, offset: int = 0, length: int | None = None) -> None:
193
+ """Flush a mapped region so the GL sees the CPU writes
194
+ (``glFlushMappedNamedBufferRange``); defaults to the whole buffer."""
195
+ self._require_alive()
196
+ self._gl.flush_mapped_named_buffer_range(self._id, offset, length or self._size)
197
+
198
+ def _delete_impl(self) -> None:
199
+ self._gl.delete_buffers([self._id])
200
+
201
+
202
+ class MappedBuffer:
203
+ """A context-managed CPU view over a mapped ``Buffer``.
204
+
205
+ Created by :meth:`Buffer.map` / :meth:`Buffer.map_range`. The mapped
206
+ address is exposed as :attr:`address`; :meth:`as_numpy` returns a
207
+ numpy view over a slice. Leaving the ``with`` block (or calling
208
+ :meth:`Buffer.unmap`) releases the mapping, after which the view must
209
+ no longer be used. When the mapping was opened without
210
+ ``GL_MAP_WRITE_BIT``, :meth:`as_numpy` views are read-only.
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ buffer: Buffer,
216
+ address: int,
217
+ offset: int,
218
+ length: int,
219
+ *,
220
+ access: int = GL_MAP_WRITE_BIT,
221
+ ) -> None:
222
+ self._buffer = buffer
223
+ self.address = address
224
+ self.offset = offset
225
+ self.length = length
226
+ self._access = access
227
+ self._closed = False
228
+
229
+ def as_numpy(self, dtype: Any, shape: tuple[int, ...], *, offset: int = 0) -> NDArray[Any]:
230
+ """Return a numpy array viewing ``shape`` elements.
231
+
232
+ ``offset`` is a byte offset within the mapped window. The array
233
+ shares the mapped memory, so writes land straight in the GPU buffer
234
+ when the mapping is writable.
235
+ """
236
+ if self._closed:
237
+ raise ValueError("mapped buffer has been closed (unmapped)")
238
+ itemsize = np.dtype(dtype).itemsize
239
+ nbytes = int(np.prod(shape)) * itemsize
240
+ if offset < 0 or nbytes < 0 or offset + nbytes > self.length:
241
+ raise ValueError(
242
+ f"as_numpy view of {nbytes} bytes at offset {offset} "
243
+ f"exceeds mapped length of {self.length} bytes"
244
+ )
245
+ ctype = np.ctypeslib.as_ctypes_type(dtype)
246
+ array = np.ctypeslib.as_array(
247
+ ctypes.cast(self.address + offset, ctypes.POINTER(ctype)), shape=shape
248
+ )
249
+ if not self._access & GL_MAP_WRITE_BIT:
250
+ array.setflags(write=False)
251
+ return array
252
+
253
+ def __enter__(self) -> MappedBuffer:
254
+ return self
255
+
256
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
257
+ if not self._closed:
258
+ self._closed = True
259
+ self._buffer.unmap()
260
+
261
+
262
+ class ShaderStorage(Buffer):
263
+ """A shader storage buffer (SSBO) bound to a binding point once.
264
+
265
+ The buffer's storage is created from ``data`` and the whole buffer is
266
+ bound to ``binding`` on ``GL_SHADER_STORAGE_BUFFER`` at construction
267
+ time; updates via :meth:`update` must not change the byte size.
268
+ """
269
+
270
+ def __init__(
271
+ self,
272
+ binding: int,
273
+ data: DataPayload,
274
+ *,
275
+ flags: int = 0,
276
+ gl: GL | None = None,
277
+ ) -> None:
278
+ flags |= GL_DYNAMIC_STORAGE_BIT
279
+ super().__init__(_payload_size(data), flags=flags, gl=gl, initial_data=data)
280
+ self._binding = binding
281
+ self.bind_range(GL_SHADER_STORAGE_BUFFER, binding)
282
+
283
+ def update(self, data: DataPayload, *, offset: int = 0) -> None:
284
+ """Upload ``data`` into the buffer at ``offset``.
285
+
286
+ The size is immutable but partial updates are allowed as long as the
287
+ region fits inside the buffer.
288
+ """
289
+ self._require_alive()
290
+ size = _payload_size(data)
291
+ if offset + size > self._size:
292
+ raise ValueError(
293
+ f"ShaderStorage update of {size} bytes at offset {offset} "
294
+ f"exceeds capacity of {self._size} bytes"
295
+ )
296
+ self._gl.named_buffer_sub_data(self._id, offset, size, data)
297
+
298
+
299
+ class EBO(Buffer):
300
+ """An element/index buffer (``GL_ELEMENT_ARRAY_BUFFER``)."""
301
+
302
+ def __init__(self, elements: np.ndarray, *, gl: GL | None = None) -> None:
303
+ if elements.dtype != np.uint32:
304
+ elements = elements.astype(np.uint32)
305
+ super().__init__(elements.nbytes, gl=gl, initial_data=elements)
306
+ self._elements = elements
307
+
308
+ def bind(self, target: int = GL_ELEMENT_ARRAY_BUFFER) -> None:
309
+ self._require_alive()
310
+ self._gl.bind_buffer(target, self._id)
311
+
312
+ def draw_count(self) -> int:
313
+ return int(self._elements.size)
314
+
315
+
316
+ class IndirectDrawBuffer(Buffer):
317
+ """An indirect draw buffer holding ``DrawArraysIndirectCommand`` records.
318
+
319
+ The buffer stores one ``(count, instanceCount, first, baseInstance)``
320
+ uint32 quadruple per draw, ready for ``glDrawArraysIndirect``.
321
+ """
322
+
323
+ def __init__(
324
+ self,
325
+ count: np.ndarray,
326
+ instance_count: np.ndarray,
327
+ first: np.ndarray,
328
+ base_instance: np.ndarray,
329
+ *,
330
+ gl: GL | None = None,
331
+ ) -> None:
332
+ n = len(count)
333
+ commands = np.zeros((n, 4), dtype=np.uint32)
334
+ commands[:, 0] = count
335
+ commands[:, 1] = instance_count
336
+ commands[:, 2] = first
337
+ commands[:, 3] = base_instance
338
+ super().__init__(commands.nbytes, gl=gl, initial_data=commands)
339
+ self._commands = commands
340
+
341
+ def bind(self, target: int = GL_DRAW_INDIRECT_BUFFER) -> None:
342
+ self._require_alive()
343
+ self._gl.bind_buffer(target, self._id)
344
+
345
+ def draw_count(self) -> int:
346
+ return len(self._commands)
347
+
348
+
349
+ IBO = IndirectDrawBuffer
350
+
351
+ __all__ = ["EBO", "IBO", "Buffer", "IndirectDrawBuffer", "MappedBuffer", "ShaderStorage"]