fheroes3/lib/batch.py

1599 lines
51 KiB
Python
Raw Normal View History

2014-06-21 12:41:07 +00:00
###############################################################################
## __init__.py ##
###############################################################################
import ctypes
import pyglet
from pyglet.gl import *
import ctypes
import re
_debug_graphics_batch = pyglet.options['debug_graphics_batch']
def _parse_data(data):
'''Given a list of data items, returns (formats, initial_arrays).'''
assert data, 'No attribute formats given'
# Return tuple (formats, initial_arrays).
formats = []
initial_arrays = []
for i, format in enumerate(data):
if isinstance(format, tuple):
format, array = format
initial_arrays.append((i, array))
formats.append(format)
formats = tuple(formats)
return formats, initial_arrays
def _get_default_batch():
shared_object_space = gl.current_context.object_space
try:
return shared_object_space.pyglet_graphics_default_batch
except AttributeError:
shared_object_space.pyglet_graphics_default_batch = Batch()
return shared_object_space.pyglet_graphics_default_batch
def vertex_list(count, *data):
'''Create a `VertexList` not associated with a batch, group or mode.
:Parameters:
`count` : int
The number of vertices in the list.
`data` : data items
Attribute formats and initial data for the vertex list. See the
module summary for details.
:rtype: `VertexList`
'''
# Note that mode=0 because the default batch is never drawn: vertex lists
# returned from this function are drawn directly by the app.
return _get_default_batch().add(count, 0, None, *data)
class DomainDraw(object):
def __init__(self, domain, mode, group):
self.domain = domain
self.mode = mode
self.group = group
def draw(self):
self.group.set_state()
self.domain.draw(self.mode)
self.group.unset_state()
class Batch(object):
'''Manage a collection of vertex lists for batched rendering.
Vertex lists are added to a `Batch` using the `add` and `add_indexed`
methods. An optional group can be specified along with the vertex list,
which gives the OpenGL state required for its rendering. Vertex lists
with shared mode and group are allocated into adjacent areas of memory and
sent to the graphics card in a single operation.
Call `VertexList.delete` to remove a vertex list from the batch.
'''
def __init__(self):
'''Create a graphics batch.'''
# Mapping to find domain.
# group -> (attributes, mode, indexed) -> domain
self.group_map = {}
# Mapping of group to list of children.
self.group_children = {}
# List of top-level groups
self.top_groups = []
self._draw_list = []
self._draw_list_dirty = False
def add(self, count, mode, group, *data):
'''Add a vertex list to the batch.
:Parameters:
`count` : int
The number of vertices in the list.
`mode` : int
OpenGL drawing mode enumeration; for example, one of
``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc.
See the module summary for additional information.
`group` : `Group`
Group of the vertex list, or ``None`` if no group is required.
`data` : data items
Attribute formats and initial data for the vertex list. See
the module summary for details.
:rtype: `VertexList`
'''
formats, initial_arrays = _parse_data(data)
domain = self._get_domain(False, mode, group, formats)
domain.__formats = formats
# Create vertex list and initialize
vlist = domain.create(count)
for i, array in initial_arrays:
vlist._set_attribute_data(i, array)
return vlist
def _get_domain(self, indexed, mode, group, formats):
if group is None:
group = null_group
# Batch group
if group not in self.group_map:
self._add_group(group)
domain_map = self.group_map[group]
# Find domain given formats, indices and mode
key = (formats, mode, indexed)
try:
domain = domain_map[key]
except KeyError:
# Create domain
if indexed:
domain = vertexdomain.create_indexed_domain(*formats)
else:
domain = create_domain(*formats)
domain_map[key] = domain
self._draw_list_dirty = True
return domain
def _add_group(self, group):
self.group_map[group] = {}
if group.parent is None:
self.top_groups.append(group)
else:
if group.parent not in self.group_map:
self._add_group(group.parent)
if group.parent not in self.group_children:
self.group_children[group.parent] = []
self.group_children[group.parent].append(group)
self._draw_list_dirty = True
def visit(self, group):
draw_list = []
# Draw domains using this group
domain_map = self.group_map[group]
for (formats, mode, indexed), domain in list(domain_map.items()):
# Remove unused domains from batch
if domain._is_empty():
del domain_map[(formats, mode, indexed)]
continue
draw_list.append(DomainDraw(domain, mode, group))
# Sort and visit child groups of this group
children = self.group_children.get(group)
if children:
children.sort()
for child in list(children):
draw_list.extend(visit(child))
if children or domain_map:
return draw_list
else:
# Remove unused group from batch
del self.group_map[group]
if group.parent:
self.group_children[group.parent].remove(group)
try:
del self.group_children[group]
except KeyError:
pass
try:
self.top_groups.remove(group)
except ValueError:
pass
return []
def _update_draw_list(self):
'''Visit group tree in preorder and create a list of bound methods
to call.
'''
self._draw_list = []
self.top_groups.sort()
for group in list(self.top_groups):
self._draw_list.extend(self.visit(group))
self._draw_list_dirty = False
if _debug_graphics_batch:
self._dump_draw_list()
def draw(self):
'''Draw the batch.
'''
if self._draw_list_dirty:
self._update_draw_list()
for func in self._draw_list:
func.draw()
class Group(object):
'''Group of common OpenGL state.
Before a vertex list is rendered, its group's OpenGL state is set; as are
that state's ancestors' states. This can be defined arbitrarily on
subclasses; the default state change has no effect, and groups vertex
lists only in the order in which they are drawn.
'''
def __init__(self, parent=None):
'''Create a group.
:Parameters:
`parent` : `Group`
Group to contain this group; its state will be set before this
state's.
'''
self.parent = parent
def set_state(self):
'''Apply the OpenGL state change.
The default implementation does nothing.'''
pass
def unset_state(self):
'''Repeal the OpenGL state change.
The default implementation does nothing.'''
pass
def set_state_recursive(self):
'''Set this group and its ancestry.
Call this method if you are using a group in isolation: the
parent groups will be called in top-down order, with this class's
`set` being called last.
'''
if self.parent:
self.parent.set_state_recursive()
self.set_state()
def unset_state_recursive(self):
'''Unset this group and its ancestry.
The inverse of `set_state_recursive`.
'''
self.unset_state()
if self.parent:
self.parent.unset_state_recursive()
class NullGroup(Group):
'''The default group class used when ``None`` is given to a batch.
This implementation has no effect.
'''
pass
#: The default group.
#:
#: :type: `Group`
null_group = NullGroup()
###############################################################################
## allocation.py ##
###############################################################################
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
# Common cases:
# -regions will be the same size (instances of same object, e.g. sprites)
# -regions will not usually be resized (only exception is text)
# -alignment of 4 vertices (glyphs, sprites, images, ...)
#
# Optimise for:
# -keeping regions adjacent, reduce the number of entries in glMultiDrawArrays
# -finding large blocks of allocated regions quickly (for drawing)
# -finding block of unallocated space is the _uncommon_ case!
#
# Decisions:
# -don't over-allocate regions to any alignment -- this would require more
# work in finding the allocated spaces (for drawing) and would result in
# more entries in glMultiDrawArrays
# -don't move blocks when they truncate themselves. try not to allocate the
# space they freed too soon (they will likely need grow back into it later,
# and growing will usually require a reallocation).
# -allocator does not track individual allocated regions. Trusts caller
# to provide accurate (start, size) tuple, which completely describes
# a region from the allocator's point of view.
# -this means that compacting is probably not feasible, or would be hideously
# expensive
class AllocatorMemoryException(Exception):
'''The buffer is not large enough to fulfil an allocation.
Raised by `Allocator` methods when the operation failed due to lack of
buffer space. The buffer should be increased to at least
requested_capacity and then the operation retried (guaranteed to
pass second time).
'''
def __init__(self, requested_capacity):
self.requested_capacity = requested_capacity
class Allocator(object):
'''Buffer space allocation implementation.'''
def __init__(self, capacity):
'''Create an allocator for a buffer of the specified capacity.
:Parameters:
`capacity` : int
Maximum size of the buffer.
'''
self.capacity = capacity
# Allocated blocks. Start index and size in parallel lists.
#
# # = allocated, - = free
#
# 0 3 5 15 20 24 40
# |###--##########-----####----------------------|
#
# starts = [0, 5, 20]
# sizes = [3, 10, 4]
#
# To calculate free blocks:
# for i in range(0, len(starts)):
# free_start[i] = starts[i] + sizes[i]
# free_size[i] = starts[i+1] - free_start[i]
# free_size[i+1] = self.capacity - free_start[-1]
self.starts = []
self.sizes = []
def set_capacity(self, size):
'''Resize the maximum buffer size.
The capaity cannot be reduced.
:Parameters:
`size` : int
New maximum size of the buffer.
'''
assert size > self.capacity
self.capacity = size
def alloc(self, size):
'''Allocate memory in the buffer.
Raises `AllocatorMemoryException` if the allocation cannot be
fulfilled.
:Parameters:
`size` : int
Size of region to allocate.
:rtype: int
:return: Starting index of the allocated region.
'''
assert size > 0
# return start
# or raise AllocatorMemoryException
if not self.starts:
if size <= self.capacity:
self.starts.append(0)
self.sizes.append(size)
return 0
else:
raise AllocatorMemoryException(size)
# Allocate in a free space
free_start = self.starts[0] + self.sizes[0]
for i, (alloc_start, alloc_size) in \
enumerate(zip(self.starts[1:], self.sizes[1:])):
# Danger!
# i is actually index - 1 because of slicing above...
# starts[i] points to the block before this free space
# starts[i+1] points to the block after this free space, and is
# always valid.
free_size = alloc_start - free_start
if free_size == size:
# Merge previous block with this one (removing this free space)
self.sizes[i] += free_size + alloc_size
del self.starts[i+1]
del self.sizes[i+1]
return free_start
elif free_size > size:
# Increase size of previous block to intrude into this free
# space.
self.sizes[i] += size
return free_start
free_start = alloc_start + alloc_size
# Allocate at end of capacity
free_size = self.capacity - free_start
if free_size >= size:
self.sizes[-1] += size
return free_start
raise AllocatorMemoryException(self.capacity + size - free_size)
def realloc(self, start, size, new_size):
'''Reallocate a region of the buffer.
This is more efficient than separate `dealloc` and `alloc` calls, as
the region can often be resized in-place.
Raises `AllocatorMemoryException` if the allocation cannot be
fulfilled.
:Parameters:
`start` : int
Current starting index of the region.
`size` : int
Current size of the region.
`new_size` : int
New size of the region.
'''
assert size > 0 and new_size > 0
# return start
# or raise AllocatorMemoryException
# Truncation is the same as deallocating the tail cruft
if new_size < size:
self.dealloc(start + new_size, size - new_size)
return start
# Find which block it lives in
for i, (alloc_start, alloc_size) in \
enumerate(zip(*(self.starts, self.sizes))):
p = start - alloc_start
if p >= 0 and size <= alloc_size - p:
break
if not (p >= 0 and size <= alloc_size - p):
print zip(self.starts, self.sizes)
print start, size, new_size
print p, alloc_start, alloc_size
assert p >= 0 and size <= alloc_size - p, 'Region not allocated'
if size == alloc_size - p:
# Region is at end of block. Find how much free space is after
# it.
is_final_block = i == len(self.starts) - 1
if not is_final_block:
free_size = self.starts[i + 1] - (start + size)
else:
free_size = self.capacity - (start + size)
# TODO If region is an entire block being an island in free space,
# can possibly extend in both directions.
if free_size == new_size - size and not is_final_block:
# Merge block with next (region is expanded in place to
# exactly fill the free space)
self.sizes[i] += free_size + self.sizes[i + 1]
del self.starts[i + 1]
del self.sizes[i + 1]
return start
elif free_size > new_size - size:
# Expand region in place
self.sizes[i] += new_size - size
return start
# The block must be repositioned. Dealloc then alloc.
# But don't do this! If alloc fails, we've already silently dealloc'd
# the original block.
# self.dealloc(start, size)
# return self.alloc(new_size)
# It must be alloc'd first. We're not missing an optimisation
# here, because if freeing the block would've allowed for the block to
# be placed in the resulting free space, one of the above in-place
# checks would've found it.
result = self.alloc(new_size)
self.dealloc(start, size)
return result
def dealloc(self, start, size):
'''Free a region of the buffer.
:Parameters:
`start` : int
Starting index of the region.
`size` : int
Size of the region.
'''
assert size > 0
assert self.starts
# Find which block needs to be split
for i, (alloc_start, alloc_size) in \
enumerate(zip(*(self.starts, self.sizes))):
p = start - alloc_start
if p >= 0 and size <= alloc_size - p:
break
# Assert we left via the break
assert p >= 0 and size <= alloc_size - p, 'Region not allocated'
if p == 0 and size == alloc_size:
# Remove entire block
del self.starts[i]
del self.sizes[i]
elif p == 0:
# Truncate beginning of block
self.starts[i] += size
self.sizes[i] -= size
elif size == alloc_size - p:
# Truncate end of block
self.sizes[i] -= size
else:
# Reduce size of left side, insert block at right side
# $ = dealloc'd block, # = alloc'd region from same block
#
# <------8------>
# <-5-><-6-><-7->
# 1 2 3 4
# #####$$$$$#####
#
# 1 = alloc_start
# 2 = start
# 3 = start + size
# 4 = alloc_start + alloc_size
# 5 = start - alloc_start = p
# 6 = size
# 7 = {8} - ({5} + {6}) = alloc_size - (p + size)
# 8 = alloc_size
#
self.sizes[i] = p
self.starts.insert(i + 1, start + size)
self.sizes.insert(i + 1, alloc_size - (p + size))
def get_allocated_regions(self):
'''Get a list of (aggregate) allocated regions.
The result of this method is ``(starts, sizes)``, where ``starts`` is
a list of starting indices of the regions and ``sizes`` their
corresponding lengths.
:rtype: (list, list)
'''
# return (starts, sizes); len(starts) == len(sizes)
return (self.starts, self.sizes)
def get_fragmented_free_size(self):
'''Returns the amount of space unused, not including the final
free block.
:rtype: int
'''
if not self.starts:
return 0
# Variation of search for free block.
total_free = 0
free_start = self.starts[0] + self.sizes[0]
for i, (alloc_start, alloc_size) in \
enumerate(zip(self.starts[1:], self.sizes[1:])):
total_free += alloc_start - free_start
free_start = alloc_start + alloc_size
return total_free
def get_free_size(self):
'''Return the amount of space unused.
:rtype: int
'''
if not self.starts:
return self.capacity
free_end = self.capacity - (self.starts[-1] + self.sizes[-1])
return self.get_fragmented_free_size() + free_end
def get_usage(self):
'''Return fraction of capacity currently allocated.
:rtype: float
'''
return 1. - self.get_free_size() / float(self.capacity)
def get_fragmentation(self):
'''Return fraction of free space that is not expandable.
:rtype: float
'''
free_size = self.get_free_size()
if free_size == 0:
return 0.
return self.get_fragmented_free_size() / float(self.get_free_size())
def _is_empty(self):
return not self.starts
def __str__(self):
return 'allocs=' + repr(zip(self.starts, self.sizes))
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, str(self))
###############################################################################
## vertexattribute.py ##
###############################################################################
_c_types = {
GL_BYTE: ctypes.c_byte,
GL_UNSIGNED_BYTE: ctypes.c_ubyte,
GL_SHORT: ctypes.c_short,
GL_UNSIGNED_SHORT: ctypes.c_ushort,
GL_INT: ctypes.c_int,
GL_UNSIGNED_INT: ctypes.c_uint,
GL_FLOAT: ctypes.c_float,
GL_DOUBLE: ctypes.c_double,
}
_gl_types = {
'b': GL_BYTE,
'B': GL_UNSIGNED_BYTE,
's': GL_SHORT,
'S': GL_UNSIGNED_SHORT,
'i': GL_INT,
'I': GL_UNSIGNED_INT,
'f': GL_FLOAT,
'd': GL_DOUBLE,
}
_attribute_format_re = re.compile(r'''
(?P<name>
[cefnstv] |
(?P<generic_index>[0-9]+) g
(?P<generic_normalized>n?))
(?P<count>[1234])
(?P<type>[bBsSiIfd])
''', re.VERBOSE)
_attribute_cache = {}
def create_attribute(format):
'''Create a vertex attribute description from a format string.
The initial stride and offset of the attribute will be 0.
:Parameters:
`format` : str
Attribute format string. See the module summary for details.
:rtype: `AbstractAttribute`
'''
try:
cls, args = _attribute_cache[format]
return cls(*args)
except KeyError:
pass
match = _attribute_format_re.match(format)
assert match, 'Invalid attribute format %r' % format
count = int(match.group('count'))
gl_type = _gl_types[match.group('type')]
generic_index = match.group('generic_index')
if generic_index:
normalized = match.group('generic_normalized')
attr_class = GenericAttribute
args = int(generic_index), normalized, count, gl_type
else:
name = match.group('name')
attr_class = _attribute_classes[name]
if attr_class._fixed_count:
assert count == attr_class._fixed_count, \
'Attributes named "%s" must have count of %d' % (
name, attr_class._fixed_count)
args = (gl_type,)
else:
args = (count, gl_type)
_attribute_cache[format] = attr_class, args
return attr_class(*args)
class AbstractAttribute(object):
'''Abstract accessor for an attribute in a mapped buffer.
'''
_fixed_count = None
def __init__(self, count, gl_type):
'''Create the attribute accessor.
:Parameters:
`count` : int
Number of components in the attribute.
`gl_type` : int
OpenGL type enumerant; for example, ``GL_FLOAT``
'''
assert count in (1, 2, 3, 4), 'Component count out of range'
self.gl_type = gl_type
self.c_type = _c_types[gl_type]
self.count = count
self.align = ctypes.sizeof(self.c_type)
self.size = count * self.align
self.stride = self.size
self.offset = 0
def enable(self):
'''Enable the attribute using ``glEnableClientState``.'''
raise NotImplementedError('abstract')
def set_pointer(self, offset):
'''Setup this attribute to point to the currently bound buffer at
the given offset.
``offset`` should be based on the currently bound buffer's ``ptr``
member.
:Parameters:
`offset` : int
Pointer offset to the currently bound buffer for this
attribute.
'''
raise NotImplementedError('abstract')
def get_region(self, buffer, start, count):
'''Map a buffer region using this attribute as an accessor.
The returned region can be modified as if the buffer was a contiguous
array of this attribute (though it may actually be interleaved or
otherwise non-contiguous).
The returned region consists of a contiguous array of component
data elements. For example, if this attribute uses 3 floats per
vertex, and the `count` parameter is 4, the number of floats mapped
will be ``3 * 4 = 12``.
:Parameters:
`buffer` : `AbstractMappable`
The buffer to map.
`start` : int
Offset of the first vertex to map.
`count` : int
Number of vertices to map
:rtype: `AbstractBufferRegion`
'''
byte_start = self.stride * start
byte_size = self.stride * count
array_count = self.count * count
if self.stride == self.size:
# non-interleaved
ptr_type = ctypes.POINTER(self.c_type * array_count)
return buffer.get_region(byte_start, byte_size, ptr_type)
else:
# interleaved
byte_start += self.offset
byte_size -= self.offset
elem_stride = self.stride // ctypes.sizeof(self.c_type)
elem_offset = self.offset // ctypes.sizeof(self.c_type)
ptr_type = ctypes.POINTER(
self.c_type * (count * elem_stride - elem_offset))
region = buffer.get_region(byte_start, byte_size, ptr_type)
return vertexbuffer.IndirectArrayRegion(
region, array_count, self.count, elem_stride)
def set_region(self, buffer, start, count, data):
'''Set the data over a region of the buffer.
:Parameters:
`buffer` : AbstractMappable`
The buffer to modify.
`start` : int
Offset of the first vertex to set.
`count` : int
Number of vertices to set.
`data` : sequence
Sequence of data components.
'''
if self.stride == self.size:
# non-interleaved
byte_start = self.stride * start
byte_size = self.stride * count
array_count = self.count * count
data = (self.c_type * array_count)(*data)
buffer.set_data_region(data, byte_start, byte_size)
else:
# interleaved
region = self.get_region(buffer, start, count)
region[:] = data
class ColorAttribute(AbstractAttribute):
'''Color vertex attribute.'''
plural = 'colors'
def __init__(self, count, gl_type):
assert count in (3, 4), 'Color attributes must have count of 3 or 4'
super(ColorAttribute, self).__init__(count, gl_type)
def enable(self):
glEnableClientState(GL_COLOR_ARRAY)
def set_pointer(self, pointer):
glColorPointer(self.count, self.gl_type, self.stride,
self.offset + pointer)
class TexCoordAttribute(AbstractAttribute):
'''Texture coordinate attribute.'''
plural = 'tex_coords'
def __init__(self, count, gl_type):
assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \
'Texture coord attribute must have non-byte signed type'
super(TexCoordAttribute, self).__init__(count, gl_type)
def enable(self):
glEnableClientState(GL_TEXTURE_COORD_ARRAY)
def set_pointer(self, pointer):
glTexCoordPointer(self.count, self.gl_type, self.stride,
self.offset + pointer)
class VertexAttribute(AbstractAttribute):
'''Vertex coordinate attribute.'''
plural = 'vertices'
def __init__(self, count, gl_type):
assert count > 1, \
'Vertex attribute must have count of 2, 3 or 4'
assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \
'Vertex attribute must have signed type larger than byte'
super(VertexAttribute, self).__init__(count, gl_type)
def enable(self):
glEnableClientState(GL_VERTEX_ARRAY)
def set_pointer(self, pointer):
glVertexPointer(self.count, self.gl_type, self.stride,
self.offset + pointer)
class GenericAttribute(AbstractAttribute):
'''Generic vertex attribute, used by shader programs.'''
def __init__(self, index, normalized, count, gl_type):
self.normalized = bool(normalized)
self.index = index
super(GenericAttribute, self).__init__(count, gl_type)
def enable(self):
glEnableVertexAttribArray(self.index)
def set_pointer(self, pointer):
glVertexAttribPointer(self.index, self.count, self.gl_type,
self.normalized, self.stride,
self.offset + pointer)
_attribute_classes = {
'c': ColorAttribute,
't': TexCoordAttribute,
'v': VertexAttribute,
}
###############################################################################
## vertexbuffer.py ##
###############################################################################
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
_enable_vbo = pyglet.options['graphics_vbo']
# Enable workaround permanently if any VBO is created on a context that has
# this workaround. (On systems with multiple contexts where one is
# unaffected, the workaround will be enabled unconditionally on all of the
# contexts anyway. This is completely unlikely anyway).
_workaround_vbo_finish = False
def create_buffer(size,
target=GL_ARRAY_BUFFER,
usage=GL_DYNAMIC_DRAW,
vbo=True):
'''Create a buffer of vertex data.
:Parameters:
`size` : int
Size of the buffer, in bytes
`target` : int
OpenGL target buffer
`usage` : int
OpenGL usage constant
`vbo` : bool
True if a `VertexBufferObject` should be created if the driver
supports it; otherwise only a `VertexArray` is created.
:rtype: `AbstractBuffer`
'''
from pyglet import gl
if (vbo and
gl_info.have_version(1, 5) and
_enable_vbo and
not gl.current_context._workaround_vbo):
return VertexBufferObject(size, target, usage)
else:
return VertexArray(size)
def create_mappable_buffer(size,
target=GL_ARRAY_BUFFER,
usage=GL_DYNAMIC_DRAW,
vbo=True):
'''Create a mappable buffer of vertex data.
:Parameters:
`size` : int
Size of the buffer, in bytes
`target` : int
OpenGL target buffer
`usage` : int
OpenGL usage constant
`vbo` : bool
True if a `VertexBufferObject` should be created if the driver
supports it; otherwise only a `VertexArray` is created.
:rtype: `AbstractBuffer` with `AbstractMappable`
'''
from pyglet import gl
if (vbo and
gl_info.have_version(1, 5) and
_enable_vbo and
not gl.current_context._workaround_vbo):
return MappableVertexBufferObject(size, target, usage)
else:
return VertexArray(size)
class AbstractBuffer(object):
'''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
`target` : int
OpenGL buffer target, for example ``GL_ARRAY_BUFFER``
`usage` : int
OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW``
'''
ptr = 0
size = 0
def bind(self):
'''Bind this buffer to its 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, invalidate=False):
'''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(object):
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
`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: `AbstractBufferRegion`
'''
raise NotImplementedError('abstract')
class VertexArray(AbstractBuffer, AbstractMappable):
'''A ctypes implementation of a vertex array.
Many of the methods on this class are effectively no-op's, such as `bind`,
`unbind`, `map`, `unmap` and `delete`; they exist in order to present
a consistent interface with `VertexBufferObject`.
This buffer type is also mappable, and so `get_region` can be used.
'''
def __init__(self, size):
self.size = size
self.array = (ctypes.c_byte * size)()
self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value
def bind(self):
pass
def unbind(self):
pass
def set_data(self, data):
ctypes.memmove(self.ptr, data, self.size)
def set_data_region(self, data, start, length):
ctypes.memmove(self.ptr + start, data, length)
def map(self, invalidate=False):
return self.array
def unmap(self):
pass
def get_region(self, start, size, ptr_type):
array = ctypes.cast(self.ptr + start, ptr_type).contents
return VertexArrayRegion(array)
def delete(self):
pass
def resize(self, size):
array = (ctypes.c_byte * size)()
ctypes.memmove(array, self.array, min(size, self.size))
self.size = size
self.array = array
self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value
class VertexBufferObject(AbstractBuffer):
'''Lightweight representation of an OpenGL VBO.
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.
This class does not implement `AbstractMappable`, and so has no
``get_region`` method. See `MappableVertexBufferObject` for a VBO class
that does implement ``get_region``.
'''
def __init__(self, size, target, usage):
self.size = size
self.target = target
self.usage = usage
self._context = pyglet.gl.current_context
id = GLuint()
glGenBuffers(1, id)
self.id = id.value
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glBindBuffer(target, self.id)
glBufferData(target, self.size, None, self.usage)
glPopClientAttrib()
global _workaround_vbo_finish
if pyglet.gl.current_context._workaround_vbo_finish:
_workaround_vbo_finish = True
def bind(self):
glBindBuffer(self.target, self.id)
def unbind(self):
glBindBuffer(self.target, 0)
def set_data(self, data):
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glBindBuffer(self.target, self.id)
glBufferData(self.target, self.size, data, self.usage)
glPopClientAttrib()
def set_data_region(self, data, start, length):
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glBindBuffer(self.target, self.id)
glBufferSubData(self.target, start, length, data)
glPopClientAttrib()
def map(self, invalidate=False):
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glBindBuffer(self.target, self.id)
if invalidate:
glBufferData(self.target, self.size, None, self.usage)
ptr = ctypes.cast(glMapBuffer(self.target, GL_WRITE_ONLY),
ctypes.POINTER(ctypes.c_byte * self.size)).contents
glPopClientAttrib()
return ptr
def unmap(self):
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glUnmapBuffer(self.target)
glPopClientAttrib()
def __del__(self):
try:
if self.id is not None:
self._context.delete_buffer(self.id)
except:
pass
def delete(self):
id = GLuint(self.id)
glDeleteBuffers(1, id)
self.id = None
def resize(self, size):
# Map, create a copy, then reinitialize.
temp = (ctypes.c_byte * size)()
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
glBindBuffer(self.target, self.id)
data = glMapBuffer(self.target, GL_READ_ONLY)
ctypes.memmove(temp, data, min(size, self.size))
glUnmapBuffer(self.target)
self.size = size
glBufferData(self.target, self.size, temp, self.usage)
glPopClientAttrib()
class AbstractBufferRegion(object):
'''A mapped region of a buffer.
Buffer regions are obtained using `AbstractMappable.get_region`.
:Ivariables:
`array` : ctypes array
Array of data, of the type and count requested by ``get_region``.
'''
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).
'''
pass
class VertexBufferObjectRegion(AbstractBufferRegion):
'''A mapped region of a VBO.'''
def __init__(self, buffer, start, end, array):
self.buffer = buffer
self.start = start
self.end = end
self.array = array
def invalidate(self):
buffer = self.buffer
buffer._dirty_min = min(buffer._dirty_min, self.start)
buffer._dirty_max = max(buffer._dirty_max, self.end)
class VertexArrayRegion(AbstractBufferRegion):
'''A mapped region of a vertex array.
The `invalidate` method is a no-op but is provided in order to present
a consistent interface with `VertexBufferObjectRegion`.
'''
def __init__(self, array):
self.array = array
###############################################################################
## vertexdomain.py ##
###############################################################################
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
_usage_format_re = re.compile(r'''
(?P<attribute>[^/]*)
(/ (?P<usage> static|dynamic|stream|none))?
''', re.VERBOSE)
_gl_usages = {
'static': GL_STATIC_DRAW,
'dynamic': GL_DYNAMIC_DRAW,
'stream': GL_STREAM_DRAW,
'none': GL_STREAM_DRAW_ARB, # Force no VBO
}
def _nearest_pow2(v):
# From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
# Credit: Sean Anderson
v -= 1
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return v + 1
def create_attribute_usage(format):
'''Create an attribute and usage pair from a format string. The
format string is as documented in `pyglet.graphics.vertexattribute`, with
the addition of an optional usage component::
usage ::= attribute ( '/' ('static' | 'dynamic' | 'stream' | 'none') )?
If the usage is not given it defaults to 'dynamic'. The usage corresponds
to the OpenGL VBO usage hint, and for ``static`` also indicates a
preference for interleaved arrays. If ``none`` is specified a buffer
object is not created, and vertex data is stored in system memory.
Some examples:
``v3f/stream``
3D vertex position using floats, for stream usage
``c4b/static``
4-byte color attribute, for static usage
:return: attribute, usage
'''
match = _usage_format_re.match(format)
attribute_format = match.group('attribute')
attribute = create_attribute(attribute_format)
usage = match.group('usage')
if usage:
vbo = not usage == 'none'
usage = _gl_usages[usage]
else:
usage = GL_DYNAMIC_DRAW
vbo = True
return (attribute, usage, vbo)
def create_domain(*attribute_usage_formats):
'''Create a vertex domain covering the given attribute usage formats.
See documentation for `create_attribute_usage` and
`pyglet.graphics.vertexattribute.create_attribute` for the grammar of
these format strings.
:rtype: `VertexDomain`
'''
attribute_usages = [create_attribute_usage(f) \
for f in attribute_usage_formats]
return VertexDomain(attribute_usages)
class VertexDomain(object):
'''Management of a set of vertex lists.
Construction of a vertex domain is usually done with the `create_domain`
function.
'''
_version = 0
_initial_count = 16
def __init__(self, attribute_usages):
self.allocator = Allocator(self._initial_count)
static_attributes = []
attributes = []
self.buffer_attributes = [] # list of (buffer, attributes)
for attribute, usage, vbo in attribute_usages:
# Create non-interleaved buffer
attributes.append(attribute)
attribute.buffer = create_mappable_buffer(
attribute.stride * self.allocator.capacity,
usage=usage, vbo=vbo)
attribute.buffer.element_size = attribute.stride
#attribute.buffer.attributes = (attribute,)
self.buffer_attributes.append(
(attribute.buffer, attribute))
# Create named attributes for each attribute
self.attributes = attributes
self.attribute_names = {}
for attribute in attributes:
if isinstance(attribute, GenericAttribute):
index = attribute.index
if 'generic' not in self.attributes:
self.attribute_names['generic'] = {}
assert index not in self.attribute_names['generic'], \
'More than one generic attribute with index %d' % index
self.attribute_names['generic'][index] = attribute
else:
name = attribute.plural
assert name not in self.attributes, \
'More than one "%s" attribute given' % name
self.attribute_names[name] = attribute
def __del__(self):
# Break circular refs that Python GC seems to miss even when forced
# collection.
for attribute in self.attributes:
del attribute.buffer
def _safe_alloc(self, count):
'''Allocate vertices, resizing the buffers if necessary.'''
try:
return self.allocator.alloc(count)
except AllocatorMemoryException, e:
capacity = _nearest_pow2(e.requested_capacity)
self._version += 1
for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size)
self.allocator.set_capacity(capacity)
return self.allocator.alloc(count)
def _safe_realloc(self, start, count, new_count):
'''Reallocate vertices, resizing the buffers if necessary.'''
try:
return self.allocator.realloc(start, count, new_count)
except AllocatorMemoryException, e:
capacity = _nearest_pow2(e.requested_capacity)
self._version += 1
for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size)
self.allocator.set_capacity(capacity)
return self.allocator.realloc(start, count, new_count)
def create(self, count):
'''Create a `VertexList` in this domain.
:Parameters:
`count` : int
Number of vertices to create.
:rtype: `VertexList`
'''
start = self._safe_alloc(count)
return VertexList(self, start, count)
def draw(self, mode):
'''Draw vertices in the domain.
If `vertex_list` is not specified, all vertices in the domain are
drawn. This is the most efficient way to render primitives.
If `vertex_list` specifies a `VertexList`, only primitives in that
list will be drawn.
:Parameters:
`mode` : int
OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
`vertex_list` : `VertexList`
Vertex list to draw, or ``None`` for all lists in this domain.
'''
starts, sizes = self.allocator.get_allocated_regions()
primcount = len(starts)
if primcount == 0:
pass
elif primcount == 1:
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
for buffer, attribute in self.buffer_attributes:
attribute.enable()
attribute.set_pointer(attribute.buffer.ptr)
if _workaround_vbo_finish:
glFinish()
# Common case
glDrawArrays(mode, starts[0], sizes[0])
glPopClientAttrib()
def _is_empty(self):
return not self.allocator.starts
def __repr__(self):
return '<%s@%x %s>' % (self.__class__.__name__, id(self),
self.allocator)
class VertexList(object):
'''A list of vertices within a `VertexDomain`. Use
`VertexDomain.create` to construct this list.
'''
def __init__(self, domain, start, count):
# TODO make private
self.domain = domain
self.start = start
self.count = count
def get_size(self):
'''Get the number of vertices in the list.
:rtype: int
'''
return self.count
def get_domain(self):
'''Get the domain this vertex list belongs to.
:rtype: `VertexDomain`
'''
return self.domain
def draw(self, mode):
'''Draw this vertex list in the given OpenGL mode.
:Parameters:
`mode` : int
OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
'''
self.domain.draw(mode, self)
def resize(self, count):
'''Resize this group.
:Parameters:
`count` : int
New number of vertices in the list.
'''
new_start = self.domain._safe_realloc(self.start, self.count, count)
if new_start != self.start:
# Copy contents to new location
for attribute in self.domain.attributes:
old = attribute.get_region(attribute.buffer,
self.start, self.count)
new = attribute.get_region(attribute.buffer,
new_start, self.count)
new.array[:] = old.array[:]
new.invalidate()
self.start = new_start
self.count = count
self._colors_cache_version = None
self._fog_coords_cache_version = None
self._edge_flags_cache_version = None
self._normals_cache_version = None
self._secondary_colors_cache_version = None
self._tex_coords_cache_version = None
self._vertices_cache_version = None
def delete(self):
'''Delete this group.'''
self.domain.allocator.dealloc(self.start, self.count)
def _set_attribute_data(self, i, data):
attribute = self.domain.attributes[i]
# TODO without region
region = attribute.get_region(attribute.buffer, self.start, self.count)
region.array[:] = data
region.invalidate()
# ---
def _get_colors(self):
if (self._colors_cache_version != self.domain._version):
domain = self.domain
attribute = domain.attribute_names['colors']
self._colors_cache = attribute.get_region(
attribute.buffer, self.start, self.count)
self._colors_cache_version = domain._version
region = self._colors_cache
region.invalidate()
return region.array
def _set_colors(self, data):
self._get_colors()[:] = data
_colors_cache = None
_colors_cache_version = None
colors = property(_get_colors, _set_colors,
doc='''Array of color data.''')
# ---
_tex_coords_cache = None
_tex_coords_cache_version = None
def _get_tex_coords(self):
if (self._tex_coords_cache_version != self.domain._version):
domain = self.domain
attribute = domain.attribute_names['tex_coords']
self._tex_coords_cache = attribute.get_region(
attribute.buffer, self.start, self.count)
self._tex_coords_cache_version = domain._version
region = self._tex_coords_cache
region.invalidate()
return region.array
def _set_tex_coords(self, data):
self._get_tex_coords()[:] = data
tex_coords = property(_get_tex_coords, _set_tex_coords,
doc='''Array of texture coordinate data.''')
# ---
_vertices_cache = None
_vertices_cache_version = None
def _get_vertices(self):
if (self._vertices_cache_version != self.domain._version):
domain = self.domain
attribute = domain.attribute_names['vertices']
self._vertices_cache = attribute.get_region(
attribute.buffer, self.start, self.count)
self._vertices_cache_version = domain._version
region = self._vertices_cache
region.invalidate()
return region.array
def _set_vertices(self, data):
self._get_vertices()[:] = data
vertices = property(_get_vertices, _set_vertices,
doc='''Array of vertex coordinate data.''')