|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import weakref |
| 4 | +from ctypes import byref, string_at |
| 5 | +from typing import TYPE_CHECKING |
| 6 | + |
| 7 | +from pyglet import gl |
| 8 | + |
| 9 | +from arcade.types import BufferProtocol |
| 10 | +from arcade.gl.new import Buffer, Context |
| 11 | + |
| 12 | +from .utils import data_to_ctypes |
| 13 | + |
| 14 | +if TYPE_CHECKING: |
| 15 | + from arcade.gl.backends.gl import GLContext |
| 16 | + |
| 17 | + |
| 18 | +class GLBuffer(Buffer): |
| 19 | + """OpenGL buffer object. Buffers store byte data and upload it |
| 20 | + to graphics memory so shader programs can process the data. |
| 21 | + They are used for storage of vertex data, |
| 22 | + element data (vertex indexing), uniform block data etc. |
| 23 | +
|
| 24 | + The ``data`` parameter can be anything that implements the |
| 25 | + `Buffer Protocol <https://docs.python.org/3/c-api/buffer.html>`_. |
| 26 | +
|
| 27 | + This includes ``bytes``, ``bytearray``, ``array.array``, and |
| 28 | + more. You may need to use typing workarounds for non-builtin |
| 29 | + types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more |
| 30 | + information. |
| 31 | +
|
| 32 | + .. warning:: Buffer objects should be created using :py:meth:`arcade.gl.Context.buffer` |
| 33 | +
|
| 34 | + Args: |
| 35 | + ctx: |
| 36 | + The context this buffer belongs to |
| 37 | + data: |
| 38 | + The data this buffer should contain. It can be a ``bytes`` instance or any |
| 39 | + object supporting the buffer protocol. |
| 40 | + reserve: |
| 41 | + Create a buffer of a specific byte size |
| 42 | + usage: |
| 43 | + A hit of this buffer is ``static`` or ``dynamic`` (can mostly be ignored) |
| 44 | + """ |
| 45 | + |
| 46 | + __slots__ = "_ctx", "_glo", "_size", "_usage", "__weakref__" |
| 47 | + _usages = { |
| 48 | + "static": gl.GL_STATIC_DRAW, |
| 49 | + "dynamic": gl.GL_DYNAMIC_DRAW, |
| 50 | + "stream": gl.GL_STREAM_DRAW, |
| 51 | + } |
| 52 | + |
| 53 | + def __init__( |
| 54 | + self, |
| 55 | + ctx: Context, |
| 56 | + data: BufferProtocol | None = None, |
| 57 | + reserve: int = 0, |
| 58 | + usage: str = "static", |
| 59 | + ) -> None: |
| 60 | + assert isinstance(ctx, GLContext) |
| 61 | + self._ctx = ctx |
| 62 | + self._glo = glo = gl.GLuint() |
| 63 | + self._size = -1 |
| 64 | + self._usage = GLBuffer._usages[usage] |
| 65 | + |
| 66 | + gl.glGenBuffers(1, byref(self._glo)) |
| 67 | + # print(f"glGenBuffers() -> {self._glo.value}") |
| 68 | + if self._glo.value == 0: |
| 69 | + raise RuntimeError("Cannot create Buffer object.") |
| 70 | + |
| 71 | + # print(f"glBindBuffer({self._glo.value})") |
| 72 | + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) |
| 73 | + # print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})") |
| 74 | + |
| 75 | + if data is not None and len(data) > 0: # type: ignore |
| 76 | + self._size, data = data_to_ctypes(data) |
| 77 | + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) |
| 78 | + elif reserve > 0: |
| 79 | + self._size = reserve |
| 80 | + # populate the buffer with zero byte values |
| 81 | + data = (gl.GLubyte * self._size)() |
| 82 | + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) |
| 83 | + else: |
| 84 | + raise ValueError("Buffer takes byte data or number of reserved bytes") |
| 85 | + |
| 86 | + if self._ctx.gc_mode == "auto": |
| 87 | + weakref.finalize(self, GLBuffer.delete_glo, self.ctx, glo) |
| 88 | + |
| 89 | + self._ctx.stats.incr("buffer") |
| 90 | + |
| 91 | + def __repr__(self): |
| 92 | + return f"<Buffer {self._glo.value}>" |
| 93 | + |
| 94 | + def __del__(self): |
| 95 | + # Intercept garbage collection if we are using Context.gc() |
| 96 | + if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: |
| 97 | + self._ctx.objects.append(self) |
| 98 | + |
| 99 | + @property |
| 100 | + def size(self) -> int: |
| 101 | + """The byte size of the buffer.""" |
| 102 | + return self._size |
| 103 | + |
| 104 | + @property |
| 105 | + def ctx(self) -> GLContext: |
| 106 | + """The context this resource belongs to.""" |
| 107 | + return self._ctx |
| 108 | + |
| 109 | + @property |
| 110 | + def glo(self) -> gl.GLuint: |
| 111 | + """The OpenGL resource id.""" |
| 112 | + return self._glo |
| 113 | + |
| 114 | + def delete(self) -> None: |
| 115 | + """ |
| 116 | + Destroy the underlying OpenGL resource. |
| 117 | +
|
| 118 | + .. warning:: Don't use this unless you know exactly what you are doing. |
| 119 | + """ |
| 120 | + GLBuffer.delete_glo(self._ctx, self._glo) |
| 121 | + self._glo.value = 0 |
| 122 | + |
| 123 | + @staticmethod |
| 124 | + def delete_glo(ctx: GLContext, glo: gl.GLuint): |
| 125 | + """ |
| 126 | + Release/delete open gl buffer. |
| 127 | +
|
| 128 | + This is automatically called when the object is garbage collected. |
| 129 | +
|
| 130 | + Args: |
| 131 | + ctx: |
| 132 | + The context the buffer belongs to |
| 133 | + glo: |
| 134 | + The OpenGL buffer id |
| 135 | + """ |
| 136 | + # If we have no context, then we are shutting down, so skip this |
| 137 | + if gl.current_context is None: |
| 138 | + return |
| 139 | + |
| 140 | + if glo.value != 0: |
| 141 | + gl.glDeleteBuffers(1, byref(glo)) |
| 142 | + glo.value = 0 |
| 143 | + |
| 144 | + ctx.stats.decr("buffer") |
| 145 | + |
| 146 | + def read(self, size: int = -1, offset: int = 0) -> bytes: |
| 147 | + """Read data from the buffer. |
| 148 | +
|
| 149 | + Args: |
| 150 | + size: |
| 151 | + The bytes to read. -1 means the entire buffer (default) |
| 152 | + offset: |
| 153 | + Byte read offset |
| 154 | + """ |
| 155 | + if size == -1: |
| 156 | + size = self._size - offset |
| 157 | + |
| 158 | + # Catch this before confusing INVALID_OPERATION is raised |
| 159 | + if size < 1: |
| 160 | + raise ValueError( |
| 161 | + "Attempting to read 0 or less bytes from buffer: " |
| 162 | + f"buffer size={self._size} | params: size={size}, offset={offset}" |
| 163 | + ) |
| 164 | + |
| 165 | + # Manually detect this so it doesn't raise a confusing INVALID_VALUE error |
| 166 | + if size + offset > self._size: |
| 167 | + raise ValueError( |
| 168 | + ( |
| 169 | + "Attempting to read outside the buffer. " |
| 170 | + f"Buffer size: {self._size} " |
| 171 | + f"Reading from {offset} to {size + offset}" |
| 172 | + ) |
| 173 | + ) |
| 174 | + |
| 175 | + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) |
| 176 | + ptr = gl.glMapBufferRange(gl.GL_ARRAY_BUFFER, offset, size, gl.GL_MAP_READ_BIT) |
| 177 | + data = string_at(ptr, size=size) |
| 178 | + gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER) |
| 179 | + return data |
| 180 | + |
| 181 | + def write(self, data: BufferProtocol, offset: int = 0): |
| 182 | + """Write byte data to the buffer from a buffer protocol object. |
| 183 | +
|
| 184 | + The ``data`` value can be anything that implements the |
| 185 | + `Buffer Protocol <https://docs.python.org/3/c-api/buffer.html>`_. |
| 186 | +
|
| 187 | + This includes ``bytes``, ``bytearray``, ``array.array``, and |
| 188 | + more. You may need to use typing workarounds for non-builtin |
| 189 | + types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more |
| 190 | + information. |
| 191 | +
|
| 192 | + If the supplied data is larger than the buffer, it will be |
| 193 | + truncated to fit. If the supplied data is smaller than the |
| 194 | + buffer, the remaining bytes will be left unchanged. |
| 195 | +
|
| 196 | + Args: |
| 197 | + data: |
| 198 | + The byte data to write. This can be bytes or any object |
| 199 | + supporting the buffer protocol. |
| 200 | + offset: |
| 201 | + The byte offset |
| 202 | + """ |
| 203 | + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) |
| 204 | + size, data = data_to_ctypes(data) |
| 205 | + # Ensure we don't write outside the buffer |
| 206 | + size = min(size, self._size - offset) |
| 207 | + if size < 0: |
| 208 | + raise ValueError("Attempting to write negative number bytes to buffer") |
| 209 | + gl.glBufferSubData(gl.GL_ARRAY_BUFFER, gl.GLintptr(offset), size, data) |
| 210 | + |
| 211 | + def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0) -> None: |
| 212 | + """Copy data into this buffer from another buffer. |
| 213 | +
|
| 214 | + Args: |
| 215 | + source: |
| 216 | + The buffer to copy from |
| 217 | + size: |
| 218 | + The amount of bytes to copy |
| 219 | + offset: |
| 220 | + The byte offset to write the data in this buffer |
| 221 | + source_offset: |
| 222 | + The byte offset to read from the source buffer |
| 223 | + """ |
| 224 | + assert isinstance(source, GLBuffer) |
| 225 | + # Read the entire source buffer into this buffer |
| 226 | + if size == -1: |
| 227 | + size = source.size |
| 228 | + |
| 229 | + # TODO: Check buffer bounds |
| 230 | + if size + source_offset > source.size: |
| 231 | + raise ValueError("Attempting to read outside the source buffer") |
| 232 | + |
| 233 | + if size + offset > self._size: |
| 234 | + raise ValueError("Attempting to write outside the buffer") |
| 235 | + |
| 236 | + gl.glBindBuffer(gl.GL_COPY_READ_BUFFER, source.glo) |
| 237 | + gl.glBindBuffer(gl.GL_COPY_WRITE_BUFFER, self._glo) |
| 238 | + gl.glCopyBufferSubData( |
| 239 | + gl.GL_COPY_READ_
F438
BUFFER, |
| 240 | + gl.GL_COPY_WRITE_BUFFER, |
| 241 | + gl.GLintptr(source_offset), # readOffset |
| 242 | + gl.GLintptr(offset), # writeOffset |
| 243 | + size, # size (number of bytes to copy) |
| 244 | + ) |
| 245 | + |
| 246 | + def orphan(self, size: int = -1, double: bool = False): |
| 247 | + """ |
| 248 | + Re-allocate the entire buffer memory. This can be used to resize |
| 249 | + a buffer or for re-specification (orphan the buffer to avoid blocking). |
| 250 | +
|
| 251 | + If the current buffer is busy in rendering operations |
| 252 | + it will be deallocated by OpenGL when completed. |
| 253 | +
|
| 254 | + Args: |
| 255 | + size: |
| 256 | + New size of buffer. -1 will retain the current size. |
| 257 | + Takes precedence over ``double`` parameter if specified. |
| 258 | + double: |
| 259 | + Is passed in with `True` the buffer size will be doubled |
| 260 | + from its current size. |
| 261 | + """ |
| 262 | + if size > 0: |
| 263 | + self._size = size |
| 264 | + elif double is True: |
| 265 | + self._size *= 2 |
| 266 | + |
| 267 | + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) |
| 268 | + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, None, self._usage) |
| 269 | + |
| 270 | + def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1): |
| 271 | + """Bind this buffer to a uniform block location. |
| 272 | + In most cases it will be sufficient to only provide a binding location. |
| 273 | +
|
| 274 | + Args: |
| 275 | + binding: |
| 276 | + The binding location |
| 277 | + offset: |
| 278 | + Byte offset |
| 279 | + size: |
| 280 | + Size of the buffer to bind. |
| 281 | + ""&qu
AD16
ot; |
| 282 | + if size < 0: |
| 283 | + size = self.size |
| 284 | + |
| 285 | + gl.glBindBufferRange(gl.GL_UNIFORM_BUFFER, binding, self._glo, offset, size) |
| 286 | + |
| 287 | + def bind_to_storage_buffer(self, binding: int = 0, offset: int = 0, size: int = -1) -> None: |
| 288 | + """ |
| 289 | + Bind this buffer as a shader storage buffer. |
| 290 | +
|
| 291 | + Args: |
| 292 | + binding: |
| 293 | + The binding location |
| 294 | + offset: |
| 295 | + Byte offset in the buffer |
| 296 | + size: |
| 297 | + The size in bytes. The entire buffer will be mapped by default. |
| 298 | + """ |
| 299 | + if size < 0: |
| 300 | + size = self.size |
| 301 | + |
| 302 | + gl.glBindBufferRange(gl.GL_SHADER_STORAGE_BUFFER, binding, self._glo, offset, size) |
0 commit comments