Data Locality

Intent: Accelerate memory access by arranging data to take advantage of CPU caching.

Acron Mapping

Need

Acron / Python approach

Contiguous position data

numpy array of floats instead of a list of NodePath objects

Bulk geometry

GeomVertexData / GeomVertexWriter — Acron’s contiguous vertex buffers

Avoiding per-object overhead

Single manager task iterating a flat list instead of per-entity tasks

Cache-friendly transforms

Cache world positions in a numpy array; sync back to NodePath only when needed

Python’s GC-managed object model makes true data locality harder than in C++, but the key wins are: avoid pointer chasing in hot loops and use numpy/array.array for bulk numeric data.

How to Implement

  1. Identify the hot loop: the code that runs for every entity every frame (positions, velocities, health ticks).

  2. Replace a list of objects with parallel numpy arrays: one for x, one for y, one for z, etc.

  3. Perform math on the arrays (vectorised numpy ops) rather than looping in Python.

  4. Write results back to NodePath positions only for visible/active entities.

  5. For geometry updates use GeomVertexWriter to write directly into Acron’s vertex buffer.

Code Sketch (Python)

import numpy as np

class BulletSystem:
    MAX = 512

    def __init__(self):
        self.count = 0
        # Contiguous arrays — cache-friendly in numpy
        self.px = np.zeros(self.MAX, dtype=np.float32)
        self.py = np.zeros(self.MAX, dtype=np.float32)
        self.vx = np.zeros(self.MAX, dtype=np.float32)
        self.vy = np.zeros(self.MAX, dtype=np.float32)
        self.alive = np.zeros(self.MAX, dtype=bool)
        # Pre-instantiated NodePaths (hidden until active)
        proto = loader.loadModel('assets/bullet.bam')
        self._nodes = []
        for i in range(self.MAX):
            np_ = proto.copyTo(render)
            np_.hide()
            self._nodes.append(np_)
        taskMgr.add(self._update, 'bulletSystem', sort=10)

    def spawn(self, x, y, vx, vy):
        if self.count >= self.MAX:
            return
        i = self.count
        self.px[i], self.py[i] = x, y
        self.vx[i], self.vy[i] = vx, vy
        self.alive[i] = True
        self._nodes[i].show()
        self.count += 1

    def _update(self, task):
        dt = globalClock.getDt()
        # Vectorised update — no Python loop over individual bullets
        mask = self.alive[:self.count]
        self.px[:self.count][mask] += self.vx[:self.count][mask] * dt
        self.py[:self.count][mask] += self.vy[:self.count][mask] * dt
        # Sync to scene graph only for alive bullets
        for i in range(self.count):
            if self.alive[i]:
                self._nodes[i].setPos(float(self.px[i]), float(self.py[i]), 0)
        return task.cont

Pitfalls

  • The bottleneck is usually the Python loop, not cache misses: in Python, vectorised numpy ops beat manual loops regardless of cache effects. Profile first with PStats before restructuring data.

  • NodePath sync cost: setPos() on hundreds of NodePath objects per frame is expensive. For truly massive counts (particles) use Acron’s particle system or instanced geometry with a custom shader instead of individual nodes.

  • GeomVertexData for geometry: when updating mesh vertex positions (water ripples, cloth), use GeomVertexWriter rather than moving many child NodePath nodes — it writes directly to the GPU buffer.