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 +99 -0
- gl46/barrier.py +82 -0
- gl46/base.py +53 -0
- gl46/buffer.py +351 -0
- gl46/camera.py +146 -0
- gl46/constants.py +361 -0
- gl46/debug.py +113 -0
- gl46/errors.py +45 -0
- gl46/framebuffer.py +233 -0
- gl46/gl.py +1342 -0
- gl46/material.py +84 -0
- gl46/pipeline.py +53 -0
- gl46/program.py +208 -0
- gl46/py.typed +0 -0
- gl46/query.py +88 -0
- gl46/renderbuffer.py +78 -0
- gl46/sampler.py +58 -0
- gl46/shader.py +103 -0
- gl46/texture.py +456 -0
- gl46/transformfeedback.py +75 -0
- gl46/utils.py +143 -0
- gl46/version.py +39 -0
- gl46/vertexarray.py +80 -0
- gl46/window.py +189 -0
- gl46/window_imgui.py +126 -0
- gl46/window_qt.py +121 -0
- gl46-0.1.0.dist-info/METADATA +276 -0
- gl46-0.1.0.dist-info/RECORD +30 -0
- gl46-0.1.0.dist-info/WHEEL +4 -0
- gl46-0.1.0.dist-info/licenses/LICENSE +21 -0
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"]
|