Memory Handlers

Most of these Classes have cdefed functions that cannot be read by Sphinx. Read the source if you want to find out more about using them.

The builtin GameSystems all use the IndexedMemoryZone to manage their memory using an arena allocation strategy to hold system data contiguously in simple C arrays. In addition, all memory will be pooled and reused instead of released. This will minimize the amount of allocation occuring during your game loop: decreasing the cost of component creation.

The zoning strategy will allow you to group entities that have similary promising habits (entities that use the same systems) into contiguous memory. If you ensure that each zone encompasses a specific type of entity aka every entity in a zone has the exact same components, everything will be initialized in such a way as to allow for processing of groups of components to be contiguous as well. If this is not strictly followed everything will still work but processing may be slightly slower as jumping around in memory may occur.

Example:

We have 10 memory units in zone ‘general’:

We have 2 entity types,

entity type 1: components: position, rotate, physics, renderer entity type 2: components: position, renderer2

Since entity 1 and entity 2 share position components but not renderer components. Position memory will be interleaved between the 2 groups while render will not.

We initialize 1 entity 1 then 5 entity 2 and then 4 more entity 1. Memory will look like. Each [] is a component in the GameSystem for [Entity number (type)].

System Name Component Memory
position [0(1)][1(2)][2(2)][3(2)][4(2)][5(2)][6(1)][7(1)][8(1)][9(1)]
rotate [0(1)][6(1)][7(1)][8(1)][9(1)][....][....][....][....][....]
physics [0(1)][6(1)][7(1)][8(1)][9(1)][....][....][....][....][....]
renderer [0(1)][6(1)][7(1)][8(1)][9(1)][....][....][....][....][....]
renderer2 [1(2)][2(2)][3(2)][4(2)][5(2)][....][....][....][....][....]

While processing renderer will have to go to memory position 0, and then jump to memory position 6 to continue through 9. If we continue interleaving types of entities in this scheme we will have to make many jumps and then go back and go through doing the same thing for renderer2.

The solution is to split the memory into zones: contiguous sections of memory reserved for a specific type of component grouping, identified by a user chosen str.

If instead we split into zones: ‘zone1’ 5 units of memory, ‘zone2’ 5 units of memory. We still use the same 10 memory units but our entities will look like: zone1 has components in position, rotate, physics, and renderer. zone2 has components in render2 and position. The share components will be split into zones. positions 0-4 in zone1 will be contained between 0-4 and positions 0-4 in zone2 will be contained between 5-9 in memory.

System Name Component Memory
position [0(1)][1(1)][2(1)][3(1)][4(1)][5(2)][6(2)][7(2)][8(2)][9(2)]
rotate [0(1)][1(1)][2(1)][3(1)][4(1)][....][....][....][....][....]
physics [0(1)][1(1)][2(1)][3(1)][4(1)][....][....][....][....][....]
renderer [0(1)][1(1)][2(1)][3(1)][4(1)][....][....][....][....][....]
renderer2 [5(2)][6(2)][7(2)][8(2)][9(2)][....][....][....][....][....]

This way when renderer is processing it can run through all of its components in a row, and when renderer2 is processing it can do the same, just a little offset into the position component array.

Buffer

class kivent_core.memory_handlers.membuffer.Buffer

The KivEnt Buffer allocates a static amount of memory and manages it by keeping a list of free_blocks. This type of memory handling is suitable for the pooling of objects. The Buffer is the only object of this type that calls malloc or free directly. The MemoryBlock, MemoryPool, and MemoryZone are all designed to allocate themselves from a Buffer or another MemoryBlock. The Buffer does not change its size.

The physical size of the buffer will be size_in_bytes and this will be split into size_in_bytes // type_size ‘blocks’.

You must call allocate_memory on a Buffer in order to actually acquire the memory. In KivEnt, this will nearly always be done as part of the GameWorld.allocate.

Attributes: (Cython Access Only)

size (unsigned int): The number of blocks (real_size / type_size) that will fit in the Buffer.

data (void*): Pointer to the beginning of the data array.

used_count (unsigned int): The number of blocks currently in use, use is either actively storing memory or in the free list waiting for reuse.

free_blocks (list): A list of previously used blocks or a contiguous collection of blocks, each entrant is a tuple of block_index, block_count.

free_block_count (unsigned int): The number of entrants in the free_blocks list, a single entrant could actually be comprised of multiple blocks, held contiguously.

data_in_free (unsigned int): The actual number of blocks held in the free_blocks list.

type_size (unsigned int): The size of a single block in bytes.

real_size (unsigned int): The size in bytes of the entire Buffer.

size_in_blocks (unsigned int): The number of blocks allocated from the parent buffer. Unused in the basic Buffer, but used by subclasses such as MemoryBlock.

MemoryBlock

class kivent_core.memory_handlers.block.MemoryBlock

The MemoryBlock is like the Buffer, except instead of allocating its memory using malloc, it gets it from either a Buffer or another MemoryBlock. It is suitable for nesting, for instance during rendering KivEnt will allocate one large MemoryBlock to represent the maximum number of frames to be rendered something like 20*512*1024 bytes:

#allocate the initial space
buffer = Buffer(100*1024*1024, 1, 1)
buffer.allocate_memory()
#allocate our first MemoryBlock, in units of bytes
mem_block = MemoryBlock(20*512*1024, 512*1024, 1)
mem_block.allocate_memory_with_buffer(buffer)
#allocate a block with the mem_block, units in 512 kib blocks
#will allocate 1 block of mem_block.type_size and split it into
#mem_block.type_size // other_type_size blocks.
mem_block2 = MemoryBlock(1, other_type_size, 512*1024)
mem_block2.allocate_memory_with_buffer(mem_block)

You must allocate with the function allocate_memory_with_buffer instead of allocate_memory. Deallocation is handled with remove_from_buffer.

Attributes: (Cython Access Only)

master_buffer (Buffer): The Buffer from which memory has been allocated. Defaults to None, will be set after allocate_memory_with_buffer has been called.

master_index (unsigned int): The location the data has been allocated at in the master_buffer. Defaults to 0, will be set after allocate_memory_with_buffer has been called.

MemoryPool

class kivent_core.memory_handlers.pool.MemoryPool

The MemoryPool is suitable for pooling C data of the same type. Memory for the objects will be allocated only once during initialization. A MemoryPool collects several MemoryBlock of the same size together to store more data than could be held in a single MemoryBlock. A position in these MemoryBlocks is referred to as a ‘slot’ and the index of this ‘slot’ gives both which block it lives in index at index // slots_per_block, and the actual index of that MemoryBlock’s data at index % slots_per_block. The data will appear contiguosly from the outside, but most likely contain a small area of excess memory in between the end of one MemoryBlock and the beginning of the next. The blocks themselves will sit contigiously. The technical size of the gap is:

MemoryBlock.real_size - MemoryBlock.count * MemoryBlock.type_size

The actual amount of slots allocated will be higher than the number the pool is initialized with to account for matching the desired size of each block.

Attributes: (Cython Access Only)

count (unsigned int): The number of total slots in the pool. Equivalent to slots_per_block * block_count.

memory_blocks (list): A list of the MemoryBlock objects. Position in this list determines which indices a MemoryBlock holds: start = slots_per_block * index in memory_blocks end = (slots_per_block * (index in memory_blocks + 1)) - 1

blocks_with_free_space (list): A list of the MemoryBlock that have open slots.

used (unsigned int): Total number of used slots, will include both active slots and slots sitting in the free list of their respective MemoryBlock.

free_count (unsigned int): Total number of slots current in the free list.

master_buffer (Buffer): The Buffer passed in on initialization that we will allocate the master_block from.

type_size (unsigned int): The size in bytes of a single slot in the pool.

master_block (MemoryBlock): The large MemoryBlock from which we will allocate the individual blocks in the pool.

block_count (unsigned int): The number of MemoryBlock this pool will have. Will be calculated on initialization using the desired_count arg: (desired_count // slots_per_block) + 1.

slots_per_block (unsigned int): The number of slots of type_size that will fit in each MemoryBlock in the pool.

MemoryZone

class kivent_core.memory_handlers.zone.MemoryZone

The MemoryZone splits one type of data storage up into several zones that are layed out contiguously using multiple MemoryPool. This way we can ensure all objects with one processing pattern are stored together. Like MemoryPool data storage will appear contiguous, however internally there may be some space in between each zone’s MemoryPool.

Attributes: (Cython Access Only)

block_size_in_kb (unsigned int): The size of each MemoryBlock that makes up the pools.

memory_pools (dict): dict storing the MemoryPool for each zone. key is the index of the MemoryPool (the order in which it was initialized).

reserved_ranges (list): list of tuples representing the start and ending indices of each individual MemoryPool.

count (unsigned int): The total number of slots available across all MemoryPool.

reserved_names (list): List storing the name of each pool, ordered by index of pool.

reserved_count (unsigned int): The total number of MemoryPool reserved.

master_buffer (Buffer): The Buffer from which all MemoryPool will be allocated.

BlockZone

class kivent_core.memory_handlers.zonedblock.BlockZone

A BlockZone manages a specific subsection of the ZonedBlock for a single zone of the data. It will store data from index start to index start + total.

Attributes: (Cython Access Only)

used_count (unsigned int): The number of memory blocks used up.

total (unsigned int): The total number of memory blocks found in this BlockZone.

start (unsigned int): The starting index of this zone in the overall ZonedBlock. The index in a specific BlockZone will be index in the ZonedBlock - this value.

data_in_free (unsigned int): The total number of blocks currently reclaimed and ready for reuse.

free_blocks (list): List managing the reclaimed blocks in the zone. Will be stored as tuples (block_index, block_count).

name (str): The name of the zone this BlockZone represents.

ZonedBlock

class kivent_core.memory_handlers.zonedblock.ZonedBlock

The ZonedBlock is like a MemoryBlock in that the data is stored contiguously. It is also somewhat like a MemoryZone in that the data is split into several regions each of which keep track of their own free list. This allows for a single contiguous block of memory that can be iterated through efficiently while still separating the memory appropriately to allow processing all data of a certain type at the same time. there will be no extra space unlike in MemoryZone and the counts for each zone will be exact.

Attributes: (Cython Access Only)

zones (dict): Dict containing the BlockZones, keyed by the name of the zone.

master_buffer (Buffer): The Buffer from which this ZonedBlock will allocate its memory.

master_index (unsigned int): The location of the data in the master_buffer

size (unsigned int): The size in bytes of the whole ZonedBlock allocation.

type_size (unsigned int): The size of each slot in the ZonedBlock in bytes.

count (unsigned int): The number of slots in the ZonedBlock, each slot will be type_size in bytes.

data (void*): Pointer to the actual data in memory.

BlockIndex

class kivent_core.memory_handlers.indexing.BlockIndex

Ties a single MemoryBlock to a set of block_count MemComponent objects, making the data held in MemoryBlock accessible from Python. See systems.staticmemgamesystem.MemComponent for a barebones implementation of such an object.

Attributes:

blocks (list): List of the objects created to wrap the data in MemoryBlock, can be accessed from Cython as block_objects. Index in this list to get a MemComponent object wrapping the data held in MemoryBlock.

PoolIndex

class kivent_core.memory_handlers.indexing.PoolIndex

The PoolIndex will generate a BlockIndex for every MemoryBlock in the MemoryPool, making the data in the entire MemoryPool accessible from Python.

Attributes:

block_indices (list): A list of the BlockIndex for the pool. Accessible in Cython via _block_indices.

ZoneIndex

class kivent_core.memory_handlers.indexing.ZoneIndex

The ZoneIndex will generate a PoolIndex for every MemoryPool in the MemoryZone, making the data in the entire MemoryZone accessible from Python.

Attributes:

pool_indices (list): A list of the PoolIndex for the zone. Accessible in Cython via _pool_indices.
get_component_from_index(self, unsigned int index)

Will retrieve a single object of the type ComponentToCreate the ZoneIndex was initialized with.

Args:
index (unsigned int): The index of the component you wish to retrieve.
Return:
object: Returns a python accessible object wrapping the data found in the MemoryZone.

IndexedMemoryZone

class kivent_core.memory_handlers.indexing.IndexedMemoryZone

An IndexedMemoryZone will create both a MemoryZone and a ZoneIndex allowing access to your data both from python (via normal list __getitem__ syntax) and cython (via get_pointer). Python slicing syntax is also supported and a list of components will be returned if a slice object is provided to __getitem__. In Python:

component_object = self[component_index]

or

component_objects = self[start_index:end_index:step]

In Cython:

cdef void* pointer = self.get_pointer(component_index)

Attributes:

memory_zone (MemoryZone): The actual MemoryZone holding the data to be indexed.

zone_index (ZoneIndex): The ZoneIndex for the memory_zone.

memrange

class kivent_core.memory_handlers.utils.memrange(IndexedMemoryZone memory_index, start=0, end=None, zone=None)

Use memrange to iterate a IndexedMemoryZone object and return the active game entities’ python objects , an active memory object is one that currently does not have the first attribute of its struct set to <unsigned int>-1. Typically KivEnt store the entity_id for the component in this position. Memory objects that have never been allocated are skipped.

Args:

memory_index (IndexedMemoryZone): The IndexedMemoryZone to iterate.

start (int): The start of iteration. Defaults 0.

end (int): The end of iteration. Defaults None. If no end is specified we will iterate all memory.

zone (str): The zone to iterate. Defaults None. If no zone is specified we will iterate through all zones.

You must reference an IndexedMemoryZone, by default we will iterate through all the memory. The area of memory iterated can be controlled with options start and end, or you can provide the name of one of the reserved zones to iterate that specific memory area.