refactor(grid): GridData SoA -- dense uint8 planes for walkable/transparent; closes #332

Cell logic storage was an array-of-UIGridPoint: 24 bytes/cell for 2 bytes of
real payload (walkable/transparent bools), the other 22 being redundant grid_x/
grid_y and a parent_grid back-pointer. Grids larger than 64 in either dimension
switched to non-contiguous 64x64 chunk storage, so no whole-grid contiguous
view was possible at any size.

Replaced with two dense row-major std::vector<uint8_t> planes on GridData
(walkable_plane, transparent_plane), indexed y*grid_w + x, with inline
is/setWalkable / is/setTransparent accessors. Chunking is dropped for LOGIC data
entirely (render caches still chunk independently in GridLayers) -- so cells are
contiguous at any size. Result: 12x smaller logic layer (2 vs 24 B/cell) and
cache-friendly iteration; a 1024x1024 grid's logic data is 2 MB instead of 24 MB.

The Python GridPoint wrapper already held (grid, x, y) and re-resolved per
access, so it just calls the new accessors -- low-invasiveness. GridData::at()
-> UIGridPoint& is removed; its callers convert to accessors:
- UIGridPoint get/set_bool_member + repr (the wrapper)
- UIGridPyMethods apply-threshold / apply-ranges heightmap writers
- syncTCODMap / syncTCODMapCell (read planes)
- EntityBehavior::isCellWalkable and PathProvider::cellWalkable (pathfinding --
  both were missed by the first scout; found via exhaustive grep)
- UITestScene

UIGridPoint's per-cell data members + ctor removed (class now only namespaces
the Python type's accessors). GridChunk.h/.cpp and ChunkManager deleted (dead
after logic-chunking removal; nothing else referenced them). No walkable/
transparent serialization existed, so no format impact.

Unblocks #333 (TCOD map dedup) and a future contiguous numpy view of
walkable/transparent.

Regression test issue_332_soa_storage_test.py: round-trips walkable/transparent
on a 200x160 grid at and across the old 64-cell chunk boundary, default values,
toggle-off, subscript vs at() agreement, grid_pos/entities, and a per-cell
footprint guard. Suite 311/311 (FOV + pathfinding + heightmap unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-07-11 01:43:30 -04:00
commit 2ceb18e673
13 changed files with 138 additions and 395 deletions

View file

@ -36,7 +36,7 @@ static BehaviorOutput executeCustom(UIEntity& entity, UIGrid& grid) {
static bool isCellWalkable(UIGrid& grid, int x, int y) {
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
return grid.at(x, y).walkable;
return grid.isWalkable(x, y); // #332
}
static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_diagonals) {

View file

@ -1,174 +0,0 @@
#include "GridChunk.h"
#include "UIGrid.h"
#include "PyTexture.h"
#include <algorithm>
#include <cmath>
// =============================================================================
// GridChunk implementation
// =============================================================================
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, GridData* parent)
: chunk_x(chunk_x), chunk_y(chunk_y),
width(width), height(height),
world_x(world_x), world_y(world_y),
cells(width * height),
dirty(true),
parent_grid(parent)
{}
UIGridPoint& GridChunk::at(int local_x, int local_y) {
return cells[local_y * width + local_x];
}
const UIGridPoint& GridChunk::at(int local_x, int local_y) const {
return cells[local_y * width + local_x];
}
void GridChunk::markDirty() {
dirty = true;
}
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
// GridChunk now only provides data storage for GridPoints
bool GridChunk::isVisible(float left_edge, float top_edge,
float right_edge, float bottom_edge) const {
// Check if chunk's cell range overlaps with viewport's cell range
float chunk_right = world_x + width;
float chunk_bottom = world_y + height;
return !(world_x >= right_edge || chunk_right <= left_edge ||
world_y >= bottom_edge || chunk_bottom <= top_edge);
}
// =============================================================================
// ChunkManager implementation
// =============================================================================
ChunkManager::ChunkManager(int grid_x, int grid_y, GridData* parent)
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
{
// Calculate number of chunks needed
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks.reserve(chunks_x * chunks_y);
// Create chunks
for (int cy = 0; cy < chunks_y; ++cy) {
for (int cx = 0; cx < chunks_x; ++cx) {
// Calculate world position
int world_x = cx * GridChunk::CHUNK_SIZE;
int world_y = cy * GridChunk::CHUNK_SIZE;
// Calculate actual size (may be smaller at edges)
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
chunks.push_back(std::make_unique<GridChunk>(
cx, cy, chunk_width, chunk_height, world_x, world_y, parent
));
}
}
}
GridChunk* ChunkManager::getChunkForCell(int x, int y) {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
return nullptr;
}
int chunk_x = x / GridChunk::CHUNK_SIZE;
int chunk_y = y / GridChunk::CHUNK_SIZE;
return getChunk(chunk_x, chunk_y);
}
const GridChunk* ChunkManager::getChunkForCell(int x, int y) const {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
return nullptr;
}
int chunk_x = x / GridChunk::CHUNK_SIZE;
int chunk_y = y / GridChunk::CHUNK_SIZE;
return getChunk(chunk_x, chunk_y);
}
GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) {
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
return nullptr;
}
return chunks[chunk_y * chunks_x + chunk_x].get();
}
const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const {
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
return nullptr;
}
return chunks[chunk_y * chunks_x + chunk_x].get();
}
UIGridPoint& ChunkManager::at(int x, int y) {
GridChunk* chunk = getChunkForCell(x, y);
if (!chunk) {
// Return a static dummy point for out-of-bounds access
// This matches the original behavior of UIGrid::at()
static UIGridPoint dummy;
return dummy;
}
// Convert to local coordinates within chunk
int local_x = x % GridChunk::CHUNK_SIZE;
int local_y = y % GridChunk::CHUNK_SIZE;
// Mark chunk dirty when accessed for modification
chunk->markDirty();
return chunk->at(local_x, local_y);
}
const UIGridPoint& ChunkManager::at(int x, int y) const {
const GridChunk* chunk = getChunkForCell(x, y);
if (!chunk) {
static UIGridPoint dummy;
return dummy;
}
int local_x = x % GridChunk::CHUNK_SIZE;
int local_y = y % GridChunk::CHUNK_SIZE;
return chunk->at(local_x, local_y);
}
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
// For now, simple rebuild - could be optimized to preserve data
grid_x = new_grid_x;
grid_y = new_grid_y;
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks.clear();
chunks.reserve(chunks_x * chunks_y);
for (int cy = 0; cy < chunks_y; ++cy) {
for (int cx = 0; cx < chunks_x; ++cx) {
int world_x = cx * GridChunk::CHUNK_SIZE;
int world_y = cy * GridChunk::CHUNK_SIZE;
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
chunks.push_back(std::make_unique<GridChunk>(
cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid
));
}
}
}
int ChunkManager::dirtyChunks() const {
int count = 0;
for (const auto& chunk : chunks) {
if (chunk->dirty) ++count;
}
return count;
}

View file

@ -1,98 +0,0 @@
#pragma once
#include "Common.h"
#include <vector>
#include <memory>
#include "UIGridPoint.h"
// Forward declarations
class UIGrid;
class GridData;
class PyTexture;
/**
* #123 - Grid chunk for sub-grid data storage
* #150 - Rendering removed; layers now handle all rendering
*
* Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid.
* Chunks store GridPoint data for pathfinding and game logic.
*/
class GridChunk {
public:
// Compile-time configurable chunk size (power of 2 recommended)
static constexpr int CHUNK_SIZE = 64;
// Position of this chunk in chunk coordinates
int chunk_x, chunk_y;
// Actual dimensions (may be less than CHUNK_SIZE at grid edges)
int width, height;
// World position (in cell coordinates)
int world_x, world_y;
// Cell data for this chunk (pathfinding properties only)
std::vector<UIGridPoint> cells;
// Dirty flag (for layer sync if needed)
bool dirty;
// Parent grid reference
GridData* parent_grid;
// Constructor
GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, GridData* parent);
// Access cell at local chunk coordinates
UIGridPoint& at(int local_x, int local_y);
const UIGridPoint& at(int local_x, int local_y) const;
// Mark chunk as dirty
void markDirty();
// Check if chunk overlaps with viewport
bool isVisible(float left_edge, float top_edge,
float right_edge, float bottom_edge) const;
};
/**
* Manages a 2D array of chunks for a grid
*/
class ChunkManager {
public:
// Dimensions in chunks
int chunks_x, chunks_y;
// Grid dimensions in cells
int grid_x, grid_y;
// All chunks (row-major order)
std::vector<std::unique_ptr<GridChunk>> chunks;
// Parent grid
GridData* parent_grid;
// Constructor - creates chunks for given grid dimensions
ChunkManager(int grid_x, int grid_y, GridData* parent);
// Get chunk containing cell (x, y)
GridChunk* getChunkForCell(int x, int y);
const GridChunk* getChunkForCell(int x, int y) const;
// Get chunk at chunk coordinates
GridChunk* getChunk(int chunk_x, int chunk_y);
const GridChunk* getChunk(int chunk_x, int chunk_y) const;
// Access cell at grid coordinates (routes through chunk)
UIGridPoint& at(int x, int y);
const UIGridPoint& at(int x, int y) const;
// Resize grid (rebuilds chunks)
void resize(int new_grid_x, int new_grid_y);
// Get total number of chunks
int totalChunks() const { return chunks_x * chunks_y; }
// Get number of dirty chunks
int dirtyChunks() const;
};

View file

@ -39,23 +39,8 @@ GridData::~GridData()
if (layer) layer->parent_grid = nullptr;
}
// #271: Null out parent_grid in all grid points (flat storage)
for (auto& p : points) {
p.parent_grid = nullptr;
}
// #277: Null out parent_grid in chunks and chunk manager
if (chunk_manager) {
for (auto& chunk : chunk_manager->chunks) {
if (chunk) {
chunk->parent_grid = nullptr;
for (auto& cell : chunk->cells) {
cell.parent_grid = nullptr;
}
}
}
chunk_manager->parent_grid = nullptr;
}
// #332: cell storage is now plain uint8 planes (no per-cell back-pointers
// to null out; they free with the vectors).
cleanupTCOD();
}
@ -73,59 +58,26 @@ void GridData::initStorage(int gx, int gy, GridData* parent_ref)
{
grid_w = gx;
grid_h = gy;
use_chunks = (gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD);
(void)parent_ref; // #332 - planes need no per-cell back-pointer
if (tcod_map) delete tcod_map;
tcod_map = new TCODMap(gx, gy);
if (use_chunks) {
chunk_manager = std::make_unique<ChunkManager>(gx, gy, parent_ref);
for (int cy = 0; cy < chunk_manager->chunks_y; ++cy) {
for (int cx = 0; cx < chunk_manager->chunks_x; ++cx) {
GridChunk* chunk = chunk_manager->getChunk(cx, cy);
if (!chunk) continue;
for (int ly = 0; ly < chunk->height; ++ly) {
for (int lx = 0; lx < chunk->width; ++lx) {
auto& cell = chunk->at(lx, ly);
cell.grid_x = chunk->world_x + lx;
cell.grid_y = chunk->world_y + ly;
cell.parent_grid = parent_ref;
}
}
}
}
} else {
points.resize(gx * gy);
for (int y = 0; y < gy; y++) {
for (int x = 0; x < gx; x++) {
int idx = y * gx + x;
points[idx].grid_x = x;
points[idx].grid_y = y;
points[idx].parent_grid = parent_ref;
}
}
}
// #332 - dense uint8 planes; default cells are not walkable / not
// transparent (matches the former UIGridPoint() default).
walkable_plane.assign((size_t)gx * (size_t)gy, 0);
transparent_plane.assign((size_t)gx * (size_t)gy, 0);
syncTCODMap();
}
// Cell access
UIGridPoint& GridData::at(int x, int y)
{
if (use_chunks && chunk_manager) {
return chunk_manager->at(x, y);
}
return points[y * grid_w + x];
}
// TCOD integration
void GridData::syncTCODMap()
{
if (!tcod_map) return;
for (int y = 0; y < grid_h; y++) {
for (int x = 0; x < grid_w; x++) {
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
tcod_map->setProperties(x, y, isTransparent(x, y), isWalkable(x, y));
}
}
fov_dirty = true;
@ -135,8 +87,7 @@ void GridData::syncTCODMap()
void GridData::syncTCODMapCell(int x, int y)
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
tcod_map->setProperties(x, y, isTransparent(x, y), isWalkable(x, y));
fov_dirty = true;
transparency_generation++;
}

View file

@ -20,7 +20,6 @@
#include "UIGridPoint.h"
#include "SpatialHash.h"
#include "GridLayers.h"
#include "GridChunk.h"
// Forward declarations
class DijkstraMap;
@ -47,16 +46,20 @@ public:
int cell_width() const { return cell_width_px; }
int cell_height() const { return cell_height_px; }
// #123 - Chunk-based storage for large grid support
std::unique_ptr<ChunkManager> chunk_manager;
// Legacy flat storage (kept for small grids or compatibility)
std::vector<UIGridPoint> points;
// Use chunks for grids larger than this threshold
static constexpr int CHUNK_THRESHOLD = 64;
bool use_chunks = false;
// #332 - Logic cell storage as two dense row-major uint8 planes (SoA).
// Replaces the 24-byte-per-cell array-of-UIGridPoint (walkable/transparent
// were only 2 bytes of payload; grid_x/grid_y/parent_grid were derivable or
// back-reference bookkeeping). 12x smaller, cache-friendly, and contiguous
// at ANY size -- chunking is dropped for logic data (render caches still
// chunk independently in GridLayers). Indexed y*grid_w + x. The Python
// GridPoint wrapper holds (grid, x, y) and reads/writes via these accessors.
std::vector<uint8_t> walkable_plane;
std::vector<uint8_t> transparent_plane;
// Cell access (handles both flat and chunked storage)
UIGridPoint& at(int x, int y);
bool isWalkable(int x, int y) const { return walkable_plane[(size_t)y * grid_w + x] != 0; }
bool isTransparent(int x, int y) const { return transparent_plane[(size_t)y * grid_w + x] != 0; }
void setWalkable(int x, int y, bool v) { walkable_plane[(size_t)y * grid_w + x] = v ? 1 : 0; }
void setTransparent(int x, int y, bool v) { transparent_plane[(size_t)y * grid_w + x] = v ? 1 : 0; }
// =========================================================================
// Entity management

View file

@ -25,7 +25,8 @@ enum class GridLayerType {
// Abstract base class for grid layers
class GridLayer {
public:
// Chunk size for per-chunk dirty tracking (matches GridChunk::CHUNK_SIZE)
// Chunk size for per-chunk render-cache dirty tracking (render-only; logic
// cell storage is dense planes on GridData since #332)
static constexpr int CHUNK_SIZE = 64;
GridLayerType type;

View file

@ -5,7 +5,7 @@
static bool cellWalkable(UIGrid& grid, int x, int y) {
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
return grid.at(x, y).walkable;
return grid.isWalkable(x, y); // #332
}
// -----------------------------------------------------------------------------

View file

@ -23,7 +23,6 @@
#include "UIDrawable.h"
#include "UIBase.h"
#include "GridLayers.h"
#include "GridChunk.h"
#include "SpatialHash.h"
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
#include "GridData.h" // #252 - Data layer base class

View file

@ -5,9 +5,9 @@
#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro
#include <cstring> // #150 - for strcmp
UIGridPoint::UIGridPoint()
: walkable(false), transparent(false), grid_x(-1), grid_y(-1), parent_grid(nullptr)
{}
// #332 - cell storage moved to GridData's dense uint8 planes; UIGridPoint no
// longer holds per-cell data. The Python GridPoint wrapper reaches walkable/
// transparent through the grid's plane accessors keyed by (x, y).
// Utility function to convert sf::Color to PyObject*
PyObject* sfColor_to_PyObject(sf::Color color) {
@ -40,56 +40,46 @@ sf::Color PyObject_to_sfColor(PyObject* obj) {
// #150 - Removed get_color/set_color - now handled by layers
// Helper to safely get the GridPoint data from coordinates
// Routes through UIGrid::at() which handles both flat and chunked storage
static UIGridPoint* getGridPointData(PyUIGridPointObject* self) {
if (!self->grid) return nullptr;
if (self->x < 0 || self->x >= self->grid->grid_w ||
self->y < 0 || self->y >= self->grid->grid_h) return nullptr;
return &self->grid->at(self->x, self->y);
// #332 - validate the wrapper's (grid, x, y) still name a live in-bounds cell.
static bool gridPointValid(PyUIGridPointObject* self) {
return self->grid &&
self->x >= 0 && self->x < self->grid->grid_w &&
self->y >= 0 && self->y < self->grid->grid_h;
}
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
auto* data = getGridPointData(self);
if (!data) {
if (!gridPointValid(self)) {
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
return NULL;
}
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
return PyBool_FromLong(data->walkable);
return PyBool_FromLong(self->grid->isWalkable(self->x, self->y));
} else { // transparent
return PyBool_FromLong(data->transparent);
return PyBool_FromLong(self->grid->isTransparent(self->x, self->y));
}
}
int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure) {
auto* data = getGridPointData(self);
if (!data) {
if (!gridPointValid(self)) {
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
return -1;
}
if (value == Py_True) {
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
data->walkable = true;
} else { // transparent
data->transparent = true;
}
} else if (value == Py_False) {
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
data->walkable = false;
} else { // transparent
data->transparent = false;
}
} else {
bool bv;
if (value == Py_True) bv = true;
else if (value == Py_False) bv = false;
else {
PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
return -1;
}
// Sync with TCOD map if parent grid exists
if (data->parent_grid && data->grid_x >= 0 && data->grid_y >= 0) {
data->parent_grid->syncTCODMapCell(data->grid_x, data->grid_y);
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
self->grid->setWalkable(self->x, self->y, bv);
} else { // transparent
self->grid->setTransparent(self->x, self->y, bv);
}
// Per-cell TCOD sync (unchanged behavior; #333 will make this lazy)
self->grid->syncTCODMapCell(self->x, self->y);
return 0;
}
@ -149,11 +139,10 @@ PyGetSetDef UIGridPoint::getsetters[] = {
PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
std::ostringstream ss;
auto* gp = getGridPointData(self);
if (!gp) ss << "<GridPoint (invalid internal object)>";
if (!gridPointValid(self)) ss << "<GridPoint (invalid internal object)>";
else {
ss << "<GridPoint (walkable=" << (gp->walkable ? "True" : "False")
<< ", transparent=" << (gp->transparent ? "True" : "False")
ss << "<GridPoint (walkable=" << (self->grid->isWalkable(self->x, self->y) ? "True" : "False")
<< ", transparent=" << (self->grid->isTransparent(self->x, self->y) ? "True" : "False")
<< ") at (" << self->x << ", " << self->y << ")>";
}
std::string repr_str = ss.str();

View file

@ -26,16 +26,14 @@ typedef struct {
int x, y; // Grid coordinates - compute data pointer on access
} PyUIGridPointObject;
// UIGridPoint - grid cell data for pathfinding and layer access
// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers
// UIGridPoint - namespaces the Python GridPoint type's accessors.
// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers.
// #332 - per-cell data (walkable/transparent) moved to GridData's dense uint8
// planes (SoA); this class no longer stores cell state. The Python wrapper
// (PyUIGridPointObject) holds (grid, x, y) and reads/writes via GridData accessors.
class UIGridPoint
{
public:
bool walkable, transparent; // Pathfinding/FOV properties
int grid_x, grid_y; // Position in parent grid
GridData* parent_grid; // Parent grid reference for TCOD sync (#252)
UIGridPoint();
// Built-in property accessors (walkable, transparent only)
static PyGetSetDef getsetters[];
static int set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure);

View file

@ -515,12 +515,11 @@ PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObj
for (int x = 0; x < self->data->grid_w; x++) {
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
if (value >= range_min && value <= range_max) {
UIGridPoint& point = self->data->at(x, y);
if (set_walkable) {
point.walkable = walkable_value;
self->data->setWalkable(x, y, walkable_value);
}
if (set_transparent) {
point.transparent = transparent_value;
self->data->setTransparent(x, y, transparent_value);
}
}
}
@ -623,15 +622,14 @@ PyObject* UIGrid::py_apply_ranges(PyUIGridObject* self, PyObject* args) {
for (int y = 0; y < self->data->grid_h; y++) {
for (int x = 0; x < self->data->grid_w; x++) {
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
UIGridPoint& point = self->data->at(x, y);
for (const auto& re : entries) {
if (value >= re.min && value <= re.max) {
if (re.set_walkable) {
point.walkable = re.walkable_value;
self->data->setWalkable(x, y, re.walkable_value);
}
if (re.set_transparent) {
point.transparent = re.transparent_value;
self->data->setTransparent(x, y, re.transparent_value);
}
break;
}

View file

@ -113,8 +113,8 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
// Use layers for visual rendering; GridPoint only has walkable/transparent
// The default "tilesprite" TileLayer is created automatically
// Example: e5->layers[0]->at(x, y) = tile_index for TileLayer
e5->points[0].walkable = true;
e5->points[0].transparent = true;
e5->setWalkable(0, 0, true); // #332
e5->setTransparent(0, 0, true);
ui_elements->push_back(e5);

View file

@ -0,0 +1,76 @@
"""Regression test for #332 - GridData SoA plane storage.
Cell walkable/transparent moved from an array-of-UIGridPoint (24 B/cell, and
chunked into non-contiguous 64x64 blocks above size 64) to two dense row-major
uint8 planes (2 B/cell, contiguous at ANY size). This verifies behavior is
preserved -- especially on LARGE grids that formerly used chunked storage, at
and across the old 64-cell chunk boundary -- and that the coordinate-keyed
GridPoint wrapper round-trips writes both directions.
Direct-execution style.
"""
import mcrfpy
import sys
import os
import gc
def rss_bytes():
with open("/proc/self/statm") as f:
return int(f.read().split()[1]) * os.sysconf("SC_PAGE_SIZE")
def main():
# Large grid: formerly chunked (>64 in both dims). Exercise cells at and
# across the old 64-cell chunk boundary -- the storage seam SoA removed.
W, H = 200, 160
g = mcrfpy.Grid(grid_size=(W, H))
cells = [(0, 0), (63, 63), (64, 64), (64, 0), (0, 64), (65, 65),
(127, 127), (128, 120), (W - 1, H - 1)]
for (x, y) in cells:
g.at(x, y).walkable = True
g.at(x, y).transparent = True
for (x, y) in cells:
gp = g.at(x, y)
assert gp.walkable is True, f"walkable round-trip failed at {(x, y)}"
assert gp.transparent is True, f"transparent round-trip failed at {(x, y)}"
# A cell never set keeps the default (False, False)
assert g.at(100, 100).walkable is False, "default walkable should be False"
assert g.at(100, 100).transparent is False, "default transparent should be False"
# Toggling back works (write False)
g.at(64, 64).walkable = False
assert g.at(64, 64).walkable is False, "toggle-off failed"
# Wrapper's coordinate-derived properties still work
assert g.at(5, 7).grid_pos == (5, 7), "grid_pos"
assert isinstance(g.at(5, 7).entities, list), "entities list"
# Subscript form reaches the same storage
g[10, 11].walkable = True
assert g.at(10, 11).walkable is True, "subscript vs at() disagree"
print(" large-grid boundary round-trip OK")
# Memory: per-cell footprint far below the old 24 B array-of-struct cell.
# Total includes the TCOD map + spatial hash, so this is a loose gross-regression
# guard, not the isolated 2 B/cell plane cost.
gc.collect()
base = rss_bytes()
N, gs = 40, 100
grids = [mcrfpy.Grid(grid_size=(gs, gs)) for _ in range(N)]
after = rss_bytes()
per_cell = (after - base) / (N * gs * gs)
print(f" ~{per_cell:.1f} bytes/cell total ({N} grids of {gs}x{gs}, incl. TCOD map)")
assert per_cell < 40, f"per-cell {per_cell:.1f} B too high -- storage regression?"
assert len(grids) == N
print("PASS")
sys.exit(0)
if __name__ == "__main__":
main()