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:
parent
5b2a412c21
commit
2ceb18e673
13 changed files with 138 additions and 395 deletions
|
|
@ -36,7 +36,7 @@ static BehaviorOutput executeCustom(UIEntity& entity, UIGrid& grid) {
|
||||||
|
|
||||||
static bool isCellWalkable(UIGrid& grid, int x, int y) {
|
static bool isCellWalkable(UIGrid& grid, int x, int y) {
|
||||||
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
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) {
|
static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_diagonals) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -39,23 +39,8 @@ GridData::~GridData()
|
||||||
if (layer) layer->parent_grid = nullptr;
|
if (layer) layer->parent_grid = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #271: Null out parent_grid in all grid points (flat storage)
|
// #332: cell storage is now plain uint8 planes (no per-cell back-pointers
|
||||||
for (auto& p : points) {
|
// to null out; they free with the vectors).
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupTCOD();
|
cleanupTCOD();
|
||||||
}
|
}
|
||||||
|
|
@ -73,59 +58,26 @@ void GridData::initStorage(int gx, int gy, GridData* parent_ref)
|
||||||
{
|
{
|
||||||
grid_w = gx;
|
grid_w = gx;
|
||||||
grid_h = gy;
|
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;
|
if (tcod_map) delete tcod_map;
|
||||||
tcod_map = new TCODMap(gx, gy);
|
tcod_map = new TCODMap(gx, gy);
|
||||||
|
|
||||||
if (use_chunks) {
|
// #332 - dense uint8 planes; default cells are not walkable / not
|
||||||
chunk_manager = std::make_unique<ChunkManager>(gx, gy, parent_ref);
|
// transparent (matches the former UIGridPoint() default).
|
||||||
for (int cy = 0; cy < chunk_manager->chunks_y; ++cy) {
|
walkable_plane.assign((size_t)gx * (size_t)gy, 0);
|
||||||
for (int cx = 0; cx < chunk_manager->chunks_x; ++cx) {
|
transparent_plane.assign((size_t)gx * (size_t)gy, 0);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncTCODMap();
|
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
|
// TCOD integration
|
||||||
void GridData::syncTCODMap()
|
void GridData::syncTCODMap()
|
||||||
{
|
{
|
||||||
if (!tcod_map) return;
|
if (!tcod_map) return;
|
||||||
for (int y = 0; y < grid_h; y++) {
|
for (int y = 0; y < grid_h; y++) {
|
||||||
for (int x = 0; x < grid_w; x++) {
|
for (int x = 0; x < grid_w; x++) {
|
||||||
const UIGridPoint& point = at(x, y);
|
tcod_map->setProperties(x, y, isTransparent(x, y), isWalkable(x, y));
|
||||||
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fov_dirty = true;
|
fov_dirty = true;
|
||||||
|
|
@ -135,8 +87,7 @@ void GridData::syncTCODMap()
|
||||||
void GridData::syncTCODMapCell(int x, int y)
|
void GridData::syncTCODMapCell(int x, int y)
|
||||||
{
|
{
|
||||||
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
|
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, isTransparent(x, y), isWalkable(x, y));
|
||||||
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
|
||||||
fov_dirty = true;
|
fov_dirty = true;
|
||||||
transparency_generation++;
|
transparency_generation++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
#include "UIGridPoint.h"
|
#include "UIGridPoint.h"
|
||||||
#include "SpatialHash.h"
|
#include "SpatialHash.h"
|
||||||
#include "GridLayers.h"
|
#include "GridLayers.h"
|
||||||
#include "GridChunk.h"
|
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class DijkstraMap;
|
class DijkstraMap;
|
||||||
|
|
@ -47,16 +46,20 @@ public:
|
||||||
int cell_width() const { return cell_width_px; }
|
int cell_width() const { return cell_width_px; }
|
||||||
int cell_height() const { return cell_height_px; }
|
int cell_height() const { return cell_height_px; }
|
||||||
|
|
||||||
// #123 - Chunk-based storage for large grid support
|
// #332 - Logic cell storage as two dense row-major uint8 planes (SoA).
|
||||||
std::unique_ptr<ChunkManager> chunk_manager;
|
// Replaces the 24-byte-per-cell array-of-UIGridPoint (walkable/transparent
|
||||||
// Legacy flat storage (kept for small grids or compatibility)
|
// were only 2 bytes of payload; grid_x/grid_y/parent_grid were derivable or
|
||||||
std::vector<UIGridPoint> points;
|
// back-reference bookkeeping). 12x smaller, cache-friendly, and contiguous
|
||||||
// Use chunks for grids larger than this threshold
|
// at ANY size -- chunking is dropped for logic data (render caches still
|
||||||
static constexpr int CHUNK_THRESHOLD = 64;
|
// chunk independently in GridLayers). Indexed y*grid_w + x. The Python
|
||||||
bool use_chunks = false;
|
// 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)
|
bool isWalkable(int x, int y) const { return walkable_plane[(size_t)y * grid_w + x] != 0; }
|
||||||
UIGridPoint& at(int x, int y);
|
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
|
// Entity management
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ enum class GridLayerType {
|
||||||
// Abstract base class for grid layers
|
// Abstract base class for grid layers
|
||||||
class GridLayer {
|
class GridLayer {
|
||||||
public:
|
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;
|
static constexpr int CHUNK_SIZE = 64;
|
||||||
|
|
||||||
GridLayerType type;
|
GridLayerType type;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
static bool cellWalkable(UIGrid& grid, int x, int y) {
|
static bool cellWalkable(UIGrid& grid, int x, int y) {
|
||||||
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
#include "UIDrawable.h"
|
#include "UIDrawable.h"
|
||||||
#include "UIBase.h"
|
#include "UIBase.h"
|
||||||
#include "GridLayers.h"
|
#include "GridLayers.h"
|
||||||
#include "GridChunk.h"
|
|
||||||
#include "SpatialHash.h"
|
#include "SpatialHash.h"
|
||||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||||
#include "GridData.h" // #252 - Data layer base class
|
#include "GridData.h" // #252 - Data layer base class
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro
|
#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
|
||||||
UIGridPoint::UIGridPoint()
|
// #332 - cell storage moved to GridData's dense uint8 planes; UIGridPoint no
|
||||||
: walkable(false), transparent(false), grid_x(-1), grid_y(-1), parent_grid(nullptr)
|
// 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*
|
// Utility function to convert sf::Color to PyObject*
|
||||||
PyObject* sfColor_to_PyObject(sf::Color color) {
|
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
|
// #150 - Removed get_color/set_color - now handled by layers
|
||||||
|
|
||||||
// Helper to safely get the GridPoint data from coordinates
|
// #332 - validate the wrapper's (grid, x, y) still name a live in-bounds cell.
|
||||||
// Routes through UIGrid::at() which handles both flat and chunked storage
|
static bool gridPointValid(PyUIGridPointObject* self) {
|
||||||
static UIGridPoint* getGridPointData(PyUIGridPointObject* self) {
|
return self->grid &&
|
||||||
if (!self->grid) return nullptr;
|
self->x >= 0 && self->x < self->grid->grid_w &&
|
||||||
if (self->x < 0 || self->x >= self->grid->grid_w ||
|
self->y >= 0 && self->y < self->grid->grid_h;
|
||||||
self->y < 0 || self->y >= self->grid->grid_h) return nullptr;
|
|
||||||
return &self->grid->at(self->x, self->y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
|
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
|
||||||
auto* data = getGridPointData(self);
|
if (!gridPointValid(self)) {
|
||||||
if (!data) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
|
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
|
} 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) {
|
int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure) {
|
||||||
auto* data = getGridPointData(self);
|
if (!gridPointValid(self)) {
|
||||||
if (!data) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (value == Py_True) {
|
bool bv;
|
||||||
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
|
if (value == Py_True) bv = true;
|
||||||
data->walkable = true;
|
else if (value == Py_False) bv = false;
|
||||||
} else { // transparent
|
else {
|
||||||
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 {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
|
PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync with TCOD map if parent grid exists
|
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
|
||||||
if (data->parent_grid && data->grid_x >= 0 && data->grid_y >= 0) {
|
self->grid->setWalkable(self->x, self->y, bv);
|
||||||
data->parent_grid->syncTCODMapCell(data->grid_x, data->grid_y);
|
} 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,11 +139,10 @@ PyGetSetDef UIGridPoint::getsetters[] = {
|
||||||
|
|
||||||
PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
|
PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
auto* gp = getGridPointData(self);
|
if (!gridPointValid(self)) ss << "<GridPoint (invalid internal object)>";
|
||||||
if (!gp) ss << "<GridPoint (invalid internal object)>";
|
|
||||||
else {
|
else {
|
||||||
ss << "<GridPoint (walkable=" << (gp->walkable ? "True" : "False")
|
ss << "<GridPoint (walkable=" << (self->grid->isWalkable(self->x, self->y) ? "True" : "False")
|
||||||
<< ", transparent=" << (gp->transparent ? "True" : "False")
|
<< ", transparent=" << (self->grid->isTransparent(self->x, self->y) ? "True" : "False")
|
||||||
<< ") at (" << self->x << ", " << self->y << ")>";
|
<< ") at (" << self->x << ", " << self->y << ")>";
|
||||||
}
|
}
|
||||||
std::string repr_str = ss.str();
|
std::string repr_str = ss.str();
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,14 @@ typedef struct {
|
||||||
int x, y; // Grid coordinates - compute data pointer on access
|
int x, y; // Grid coordinates - compute data pointer on access
|
||||||
} PyUIGridPointObject;
|
} PyUIGridPointObject;
|
||||||
|
|
||||||
// UIGridPoint - grid cell data for pathfinding and layer access
|
// UIGridPoint - namespaces the Python GridPoint type's accessors.
|
||||||
// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers
|
// #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
|
class UIGridPoint
|
||||||
{
|
{
|
||||||
public:
|
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)
|
// Built-in property accessors (walkable, transparent only)
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static int set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure);
|
static int set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -515,12 +515,11 @@ PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObj
|
||||||
for (int x = 0; x < self->data->grid_w; x++) {
|
for (int x = 0; x < self->data->grid_w; x++) {
|
||||||
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
||||||
if (value >= range_min && value <= range_max) {
|
if (value >= range_min && value <= range_max) {
|
||||||
UIGridPoint& point = self->data->at(x, y);
|
|
||||||
if (set_walkable) {
|
if (set_walkable) {
|
||||||
point.walkable = walkable_value;
|
self->data->setWalkable(x, y, walkable_value);
|
||||||
}
|
}
|
||||||
if (set_transparent) {
|
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 y = 0; y < self->data->grid_h; y++) {
|
||||||
for (int x = 0; x < self->data->grid_w; x++) {
|
for (int x = 0; x < self->data->grid_w; x++) {
|
||||||
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
float value = TCOD_heightmap_get_value(hmap->heightmap, x, y);
|
||||||
UIGridPoint& point = self->data->at(x, y);
|
|
||||||
|
|
||||||
for (const auto& re : entries) {
|
for (const auto& re : entries) {
|
||||||
if (value >= re.min && value <= re.max) {
|
if (value >= re.min && value <= re.max) {
|
||||||
if (re.set_walkable) {
|
if (re.set_walkable) {
|
||||||
point.walkable = re.walkable_value;
|
self->data->setWalkable(x, y, re.walkable_value);
|
||||||
}
|
}
|
||||||
if (re.set_transparent) {
|
if (re.set_transparent) {
|
||||||
point.transparent = re.transparent_value;
|
self->data->setTransparent(x, y, re.transparent_value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
||||||
// Use layers for visual rendering; GridPoint only has walkable/transparent
|
// Use layers for visual rendering; GridPoint only has walkable/transparent
|
||||||
// The default "tilesprite" TileLayer is created automatically
|
// The default "tilesprite" TileLayer is created automatically
|
||||||
// Example: e5->layers[0]->at(x, y) = tile_index for TileLayer
|
// Example: e5->layers[0]->at(x, y) = tile_index for TileLayer
|
||||||
e5->points[0].walkable = true;
|
e5->setWalkable(0, 0, true); // #332
|
||||||
e5->points[0].transparent = true;
|
e5->setTransparent(0, 0, true);
|
||||||
|
|
||||||
ui_elements->push_back(e5);
|
ui_elements->push_back(e5);
|
||||||
|
|
||||||
|
|
|
||||||
76
tests/regression/issue_332_soa_storage_test.py
Normal file
76
tests/regression/issue_332_soa_storage_test.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue