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