314 lines
10 KiB
Python
314 lines
10 KiB
Python
"""Byte abstractions of OpenGL Buffer Objects.
|
|
|
|
Use `create_buffer` to create a Buffer Object.
|
|
|
|
Buffers can optionally be created "mappable" (incorporating the
|
|
`AbstractMappable` mix-in). In this case the buffer provides a ``get_region``
|
|
method which provides the most efficient path for updating partial data within
|
|
the buffer.
|
|
"""
|
|
|
|
import sys
|
|
import ctypes
|
|
|
|
import pyglet
|
|
from pyglet.gl import *
|
|
|
|
|
|
class AbstractBuffer:
|
|
"""Abstract buffer of byte data.
|
|
|
|
:Ivariables:
|
|
`size` : int
|
|
Size of buffer, in bytes
|
|
`ptr` : int
|
|
Memory offset of the buffer, as used by the ``glVertexPointer``
|
|
family of functions
|
|
`usage` : int
|
|
OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW``
|
|
|
|
"""
|
|
|
|
ptr = 0
|
|
size = 0
|
|
|
|
def bind(self, target=GL_ARRAY_BUFFER):
|
|
"""Bind this buffer to an OpenGL target."""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def unbind(self):
|
|
"""Reset the buffer's OpenGL target."""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def set_data(self, data):
|
|
"""Set the entire contents of the buffer.
|
|
|
|
:Parameters:
|
|
`data` : sequence of int or ctypes pointer
|
|
The byte array to set.
|
|
|
|
"""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def set_data_region(self, data, start, length):
|
|
"""Set part of the buffer contents.
|
|
|
|
:Parameters:
|
|
`data` : sequence of int or ctypes pointer
|
|
The byte array of data to set
|
|
`start` : int
|
|
Offset to start replacing data
|
|
`length` : int
|
|
Length of region to replace
|
|
|
|
"""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def map(self):
|
|
"""Map the entire buffer into system memory.
|
|
|
|
The mapped region must be subsequently unmapped with `unmap` before
|
|
performing any other operations on the buffer.
|
|
|
|
:Parameters:
|
|
`invalidate` : bool
|
|
If True, the initial contents of the mapped block need not
|
|
reflect the actual contents of the buffer.
|
|
|
|
:rtype: ``POINTER(ctypes.c_ubyte)``
|
|
:return: Pointer to the mapped block in memory
|
|
"""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def unmap(self):
|
|
"""Unmap a previously mapped memory block."""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def resize(self, size):
|
|
"""Resize the buffer to a new size.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
New size of the buffer, in bytes
|
|
|
|
"""
|
|
|
|
def delete(self):
|
|
"""Delete this buffer, reducing system resource usage."""
|
|
raise NotImplementedError('abstract')
|
|
|
|
|
|
class AbstractMappable:
|
|
|
|
def get_region(self, start, size, ptr_type):
|
|
"""Map a region of the buffer into a ctypes array of the desired
|
|
type. This region does not need to be unmapped, but will become
|
|
invalid if the buffer is resized.
|
|
|
|
Note that although a pointer type is required, an array is mapped.
|
|
For example::
|
|
|
|
get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20))
|
|
|
|
will map bytes 0 to 80 of the buffer to an array of 20 ints.
|
|
|
|
Changes to the array may not be recognised until the region's
|
|
:py:meth:`AbstractBufferRegion.invalidate` method is called.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
Offset into the buffer to map from, in bytes
|
|
`size` : int
|
|
Size of the buffer region to map, in bytes
|
|
`ptr_type` : ctypes pointer type
|
|
Pointer type describing the array format to create
|
|
|
|
:rtype: :py:class:`AbstractBufferRegion`
|
|
"""
|
|
raise NotImplementedError('abstract')
|
|
|
|
|
|
class BufferObject(AbstractBuffer):
|
|
"""Lightweight representation of an OpenGL Buffer Object.
|
|
|
|
The data in the buffer is not replicated in any system memory (unless it
|
|
is done so by the video driver). While this can improve memory usage and
|
|
possibly performance, updates to the buffer are relatively slow.
|
|
The target of the buffer is ``GL_ARRAY_BUFFER`` internally to avoid
|
|
accidentally overriding other states when altering the buffer contents.
|
|
The intended target can be set when binding the buffer.
|
|
|
|
This class does not implement :py:class:`AbstractMappable`, and so has no
|
|
:py:meth:`~AbstractMappable.get_region` method. See
|
|
:py:class:`MappableVertexBufferObject` for a Buffer class
|
|
that does implement :py:meth:`~AbstractMappable.get_region`.
|
|
"""
|
|
|
|
def __init__(self, size, usage=GL_DYNAMIC_DRAW):
|
|
self.size = size
|
|
self.usage = usage
|
|
self._context = pyglet.gl.current_context
|
|
|
|
buffer_id = GLuint()
|
|
glGenBuffers(1, buffer_id)
|
|
self.id = buffer_id.value
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
data = (GLubyte * self.size)()
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, data, self.usage)
|
|
|
|
def invalidate(self):
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, None, self.usage)
|
|
|
|
def bind(self, target=GL_ARRAY_BUFFER):
|
|
glBindBuffer(target, self.id)
|
|
|
|
def unbind(self):
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
|
|
def bind_to_index_buffer(self):
|
|
"""Binds this buffer as an index buffer on the active vertex array."""
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.id)
|
|
|
|
def set_data(self, data):
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, data, self.usage)
|
|
|
|
def set_data_region(self, data, start, length):
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
glBufferSubData(GL_ARRAY_BUFFER, start, length, data)
|
|
|
|
def map(self):
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY), ctypes.POINTER(ctypes.c_byte * self.size)).contents
|
|
return ptr
|
|
|
|
def map_range(self, start, size, ptr_type):
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
ptr = ctypes.cast(glMapBufferRange(GL_ARRAY_BUFFER, start, size, GL_MAP_WRITE_BIT), ptr_type).contents
|
|
return ptr
|
|
|
|
def unmap(self):
|
|
glUnmapBuffer(GL_ARRAY_BUFFER)
|
|
|
|
def __del__(self):
|
|
try:
|
|
if self.id is not None:
|
|
self._context.delete_buffer(self.id)
|
|
except:
|
|
pass
|
|
|
|
def delete(self):
|
|
buffer_id = GLuint(self.id)
|
|
try:
|
|
glDeleteBuffers(1, buffer_id)
|
|
except Exception:
|
|
pass
|
|
self.id = None
|
|
|
|
def resize(self, size):
|
|
# Map, create a copy, then reinitialize.
|
|
temp = (ctypes.c_byte * size)()
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
data = glMapBufferRange(GL_ARRAY_BUFFER, 0, self.size, GL_MAP_READ_BIT)
|
|
ctypes.memmove(temp, data, min(size, self.size))
|
|
glUnmapBuffer(GL_ARRAY_BUFFER)
|
|
|
|
self.size = size
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, temp, self.usage)
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}(id={self.id}, size={self.size})"
|
|
|
|
|
|
class MappableBufferObject(BufferObject, AbstractMappable):
|
|
"""A buffer with system-memory backed store.
|
|
|
|
Updates to the data via `set_data`, `set_data_region` and `map` will be
|
|
held in local memory until `bind` is called. The advantage is that fewer
|
|
OpenGL calls are needed, increasing performance.
|
|
|
|
There may also be less performance penalty for resizing this buffer.
|
|
|
|
Updates to data via :py:meth:`map` are committed immediately.
|
|
"""
|
|
def __init__(self, size, usage=GL_DYNAMIC_DRAW):
|
|
super(MappableBufferObject, self).__init__(size, usage)
|
|
self.data = (ctypes.c_byte * size)()
|
|
self.data_ptr = ctypes.addressof(self.data)
|
|
self._dirty_min = sys.maxsize
|
|
self._dirty_max = 0
|
|
|
|
def bind(self):
|
|
# Commit pending data
|
|
super(MappableBufferObject, self).bind()
|
|
size = self._dirty_max - self._dirty_min
|
|
if size > 0:
|
|
if size == self.size:
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, self.data, self.usage)
|
|
else:
|
|
glBufferSubData(GL_ARRAY_BUFFER, self._dirty_min, size, self.data_ptr + self._dirty_min)
|
|
self._dirty_min = sys.maxsize
|
|
self._dirty_max = 0
|
|
|
|
def set_data(self, data):
|
|
super(MappableBufferObject, self).set_data(data)
|
|
ctypes.memmove(self.data, data, self.size)
|
|
self._dirty_min = 0
|
|
self._dirty_max = self.size
|
|
|
|
def set_data_region(self, data, start, length):
|
|
ctypes.memmove(self.data_ptr + start, data, length)
|
|
self._dirty_min = min(start, self._dirty_min)
|
|
self._dirty_max = max(start + length, self._dirty_max)
|
|
|
|
def map(self, invalidate=False):
|
|
self._dirty_min = 0
|
|
self._dirty_max = self.size
|
|
return self.data
|
|
|
|
def unmap(self):
|
|
pass
|
|
|
|
def get_region(self, start, size, ptr_type):
|
|
array = ctypes.cast(self.data_ptr + start, ptr_type).contents
|
|
return BufferObjectRegion(self, start, start + size, array)
|
|
|
|
def resize(self, size):
|
|
data = (ctypes.c_byte * size)()
|
|
ctypes.memmove(data, self.data, min(size, self.size))
|
|
self.data = data
|
|
self.data_ptr = ctypes.addressof(self.data)
|
|
|
|
self.size = size
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
|
glBufferData(GL_ARRAY_BUFFER, self.size, self.data, self.usage)
|
|
|
|
self._dirty_min = sys.maxsize
|
|
self._dirty_max = 0
|
|
|
|
|
|
class BufferObjectRegion:
|
|
"""A mapped region of a MappableBufferObject."""
|
|
|
|
__slots__ = 'buffer', 'start', 'end', 'array'
|
|
|
|
def __init__(self, buffer, start, end, array):
|
|
self.buffer = buffer
|
|
self.start = start
|
|
self.end = end
|
|
self.array = array
|
|
|
|
def invalidate(self):
|
|
"""Mark this region as changed.
|
|
|
|
The buffer may not be updated with the latest contents of the
|
|
array until this method is called. (However, it may not be updated
|
|
until the next time the buffer is used, for efficiency).
|
|
"""
|
|
buffer = self.buffer
|
|
buffer._dirty_min = min(buffer._dirty_min, self.start)
|
|
buffer._dirty_max = max(buffer._dirty_max, self.end)
|