GridView and Grid, FOVLayer and PathLayer #252
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#270 [Bugfix] GridLayer::parent_grid is a dangling raw pointer when grid is destroyed
john/McRogueFace
#271 [Bugfix] UIGridPoint::parent_grid is a dangling raw pointer
john/McRogueFace
#277 [Bugfix] GridChunk::parent_grid raw pointer can dangle
john/McRogueFace
Reference
john/McRogueFace#252
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
7DRL finding. The requirement to size and position a Grid when it's really serving as a container of walkable/visible data, the requirement to use a single visible/walkable dataset, and the requirement to allocate the walkable/visible layers regardless of if or how they're being used, are all huge disappointments with the current API.
first, an investigation.
Then, in "complete harmony" with the
HeightMapandDiscreteMapclasses and libtcod's underlying walkable, visible datastructures - let's provide the currentGridexperience as aGridViewwith a defaultGridwith defaultvisibleandwalkablelayers. Directly instantiating aGridmeans shared map data, with multiple or no tcod map layers inside of it; multipleGridViewinstances can share the texture cache with independent camera positions.Draft Plan: GridMap/GridView/Grid Split (#252)
Context
During 7DRL 2026, the monolithic
UIGridclass proved frustrating: every grid requires positioning/sizing/zoom even when it's just a data container, walkable/transparent are hardcoded to one-each-per-grid, and there's no way to share map data between views. Issue #252 calls for splitting Grid into a data container and a rendering viewport, while modernizing how walkable/transparent/cost maps integrate with the procgen pipeline (DiscreteMap, HeightMap, BSP, Noise, WangSet).Three-Type Design
GridMapGridViewGridGridpreserves the current API:mcrfpy.Grid(grid_size=..., pos=..., size=...)works as before but the returned object is a GridView withgrid.gridmapexposing the underlying GridMap. Existing code doesn't break. Power users access GridMap directly for multi-view or shared-data scenarios.DiscreteMap as Universal Substrate
Walkable, transparent, and cost data become named DiscreteMap slots on GridMap. No new types needed. TCODMap is rebuilt lazily from named slots when FOV/pathfinding is requested.
Default slots created automatically:
"walkable"(fill=1),"transparent"(fill=1).Multiple maps per grid for different entity types:
Python API
Architecture
What Goes Where
grid_w, grid_hptex(texture)tcod_mappoints[]/chunk_managerentitiesspatial_hashchildrenlayersdijkstra_mapsrenderTexturerotationTexturebox, fill_colorcenter_x/y, zoomcamera_rotationperspective_entity/enabledon_cell_*callbacksscreenToCell()Layer Chunk Caches: Shared, Not Per-View
Chunk rendering depends on layer data + cell dimensions (from GridMap's texture), NOT on camera position. Camera transform is applied AFTER chunks are drawn. So layers keep their existing chunk cache architecture — multiple GridViews rendering the same GridMap reuse the same chunk textures. No per-view cache needed.
The only change:
GridLayer::parent_gridchanges fromUIGrid*toGridMap*.GridPoint Becomes a Proxy
UIGridPointloses itsbool walkable, transparentmembers. Instead,gridmap.at(x,y)returns a lightweight proxy that reads/writes the GridMap's named DiscreteMap slots:The proxy stores
(GridMap*, x, y)instead of owning data.DiscreteMap Zero-Copy Binding
Add an
ownerfield toPyDiscreteMapObjectso GridMap slots can be exposed as DiscreteMap views without copying:GridMap stores raw
uint8_t*arrays for its map slots.gridmap.map("walkable")returns a DiscreteMap that wraps the GridMap's internal array. Mutations trigger a dirty flag for lazy TCODMap rebuild.Cost-Weighted Pathfinding
Use libtcod's
ITCODPathCallbackto read from a DiscreteMap cost field:Entity Changes
UIEntity::gridchanges fromshared_ptr<UIGrid>toshared_ptr<GridMap>. Entity is already conceptualized as "a perspective on Grid data" (see UIEntity.h comment block). The gridstate (visible/discovered per cell) stays on Entity.Entity gains optional per-entity map preferences:
perspective_walkable— name of walkable map for this entity's pathfindingperspective_transparent— name of transparent map for this entity's FOVPerspective System
Currently duplicated: ColorLayer has perspective binding AND UIGrid has perspective_entity. In the new design, perspective is purely a GridView concern. GridView's
render()applies FOV overlay based on itsperspective_entity, using that entity'sperspective_transparentmap for FOV computation. ColorLayer's perspective methods (apply_perspective,update_perspective) are deprecated in favor of view-level perspective.Grid Compatibility Shim
mcrfpy.Gridbecomes either:__init__accepts the combined parameter set (grid_size + pos + size + texture + ...) and auto-creates a GridMap, ORSubclass approach is cleaner:
isinstance(obj, GridView)is True, existing code that checks type still works, and Grid can override__init__to accept the legacy parameter signature while delegating to GridView.Methods like
grid.compute_fov(),grid.find_path(),grid.at(),grid.map(),grid.entities,grid.layersdelegate toself.gridmap.Files
New Files
src/GridMap.hGridMapclass — data containersrc/GridMap.cppsrc/PyGridMap.hmcrfpy.GridMapsrc/PyGridMap.cppsrc/GridView.hGridViewclass — UIDrawable viewportsrc/GridView.cppsrc/PyGridView.hmcrfpy.GridView+mcrfpy.Gridshimsrc/PyGridView.cppModified Files
src/PyDiscreteMap.h/cppownerfield, dirty callbacksrc/UIEntity.h/cppshared_ptr<UIGrid>->shared_ptr<GridMap>, add perspective map namessrc/UIEntityCollection.h/cppsrc/UIGridPoint.h/cppsrc/GridLayers.h/cppUIGrid*->GridMap*parent referencesrc/UIGridPathfinding.h/cppsrc/SpatialHash.h/cppsrc/McRFPy_API.cppsrc/UIDrawable.hUIGRIDVIEWto PyObjectsEnumsrc/Scene.h/cppCMakeLists.txtRemoved Files
src/UIGrid.hsrc/GridMap.h+src/GridView.hsrc/UIGrid.cppsrc/GridMap.cpp+src/GridView.cppsrc/GridChunk.h/cppImplementation Phases
Phase 0: Foundation (non-breaking)
ownerfield to PyDiscreteMapObject (view support, dirty callback)Phase 1: GridMap Class
GridMapwith: dimensions, named map slots (default "walkable"/"transparent"), layer management, entity list + spatial hash, children collectionwalkable(fill=1),transparent(fill=1)mcrfpy.GridMap— constructor takesgrid_size,textureat(),map(),add_map(),set_map(),remove_map(),add_layer(),remove_layer(),layer()Phase 2: Pathfinding on GridMap
walkable=,transparent=,cost=named-map parameters tocompute_fov(),find_path(),get_dijkstra_map()DiscreteMapCostCallbackfor weighted pathfindingPhase 3: GridView Class
GridViewinheriting UIDrawable, referencingshared_ptr<GridMap>gridmapproperty exposes the underlying GridMapmcrfpy.GridViewwith all current Grid rendering propertiesPhase 4: Grid Compatibility Shim
mcrfpy.Gridas subclass of GridViewgrid_size+pos+size+texture+ all current kwargsself.gridmapat(),map(),compute_fov(),find_path(),entities,layers,childrenPhase 5: Entity + Layer Migration
UIEntity::gridfromshared_ptr<UIGrid>toshared_ptr<GridMap>perspective_walkable,perspective_transparentstring properties to EntityGridLayer::parent_gridfromUIGrid*toGridMap*Phase 6: Cleanup
Verification
Unit Tests (new)
GridMap:map("walkable")returns writable DiscreteMap, modifications reflected in pathfindingGridMap:add_map("custom")creates slot, usable infind_path(walkable="custom")GridMap:at(x,y).walkableproxy reads/writes walkable DiscreteMap slotGridView(gridmap=...)renders correctlyGridViews sharing oneGridMapshow same data with different camerascompute_fov()produces different FOVperspective_walkable/perspective_transparentused by perspective systemBackwards Compatibility (critical)
mcrfpy.Grid(grid_size=..., pos=..., size=...)returns working GridViewgrid.at(x,y).walkable,grid.compute_fov(),grid.find_path()all workgrid.entities,grid.layers,grid.childrenall workscene.children.append(grid)works (Grid IS-A GridView IS-A UIDrawable)Integration Tests
Manual Verification
make && cd build && ./mcrogueface --headless --exec ../tests/unit/grid_view_test.pycd tests && python3 run_tests.pyRisks and Concerns
This is a complex architectural change which has the potential to undermine a huge amount of memory safety progress that McRogueFace has made since the plan was drafted. See these make options:
While rearchitecting, we should perform sanitization and debug builds incrementally. Waiting until the end to perform memory safety checks isn't a good strategy - every compilation attempt during the architecture overhaul is also a good opportunity to find memory errors, including instrumentation with libtcod, since the grid system is so deeply connected to that library.
I considered the plan above basically ready to implement, but instead I started working on instrumentation to find memory safety issues. I might execute this plan in conjunction with some RAII templates or macros to encourage memory and refcount safety in all of McRogueFace's C++ to Python interactions.
7DRL Findings: Additional scope for this overhaul
Post-jam analysis identified several grid/entity pain points that belong under this issue rather than as standalone work:
Entity Gridstate Lifecycle (Items 1, 5, 6 from jam feedback)
Problem:
ensureGridstate()silently destroys exploration data on grid resize, retains stale data for same-size grids, and there's no save/load mechanism.entity.gridstatereturns detached SimpleNamespace copies whileentity.at()returns live proxies — no batch operations exist, forcing O(w*h) Python iteration for save/restore.Resolution under #252: With the Grid/GridView split, gridstate data lives on the Grid (data container). Entity perspective becomes a DiscreteMap reference on the entity, with convenience methods for swapping/overwriting when entities change grids. Game developers who want per-grid memory caching can manage DiscreteMap instances in Python — the engine provides the primitives (correctly-sized DiscreteMap creation, assignment with validation, automatic disabling on grid departure) rather than a built-in cache. See planned issue for
Entity.gridstateas DiscreteMap.FOV Query / Overlay Decoupling (Items 2, 3 from jam feedback)
Problem:
grid.perspective = entityconflates two independent concerns: (1) FOV spatial queries (is_in_fov) and (2) per-frame alpha overlay rendering. When using ColorLayer fog exclusively, you can't get FOV queries without the overlay. When both systems are active, alpha values stack (ColorLayer α=150 + perspective overlay α=192 = double-darkening).Resolution under #252: The
FOVLayerconcept resolves this architecturally — FOV visualization becomes an explicit, optional layer rather than an implicit overlay baked into grid.perspective. The Grid owns FOV computation data (which entity is the perspective source); the GridView controls whether/how it renders.cell_pos— integer logical position decoupled from render position #295Roadmap context
This issue is part of the Grid & Entity Overhaul Roadmap (
docs/GRID_ENTITY_OVERHAUL_ROADMAP.md), placed in Phase 4 (Grid Architecture).Phase 4 is sequenced after the behavior system (Phases 2-3) so the data model is validated before the architecture shift. The behavior system (
grid.step(),cell_pos, etc.) lives on the data Grid, not the renderer, so it survives the split with minimal rework.Dangling pointer fixes #270, #271, #277 are folded into this issue — converting raw
UIGrid*toweak_ptr<Grid>during the split.Related new issues: #295 (cell_pos), #296 (labels), #297-#303 (behavior system). See roadmap for full dependency graph."
Grid/GridView API Unification — Implemented
Commit
109bc21implements the core unification.mcrfpy.Grid()now returns aGridViewthat internally owns aGridData(UIGrid). The oldUIGridtype is renamed to_GridData(internal).What changed
10 files, +612/-142 lines:
tp_getattro/tp_setattrodelegation to underlying UIGrid, PythonObjectCache registration, animation property support (center, zoom, shader.*)tp_namechanged tomcrfpy._GridData(internal)owning_viewweak_ptr back-reference for Entity.grid reverse lookupgridgetter returns GridView wrapper; setter accepts GridView;find_pathuses internal type directlyremoveFromParent()handles UIGRIDVIEW (accesses children via grid_data, not invalid static_cast); ~12 switch cases added for UIGRIDVIEWColorLayer_set_gridandTileLayer_set_gridextract UIGrid from GridView correctlyTest results
263/263 tests pass (100%). Several bugs were found and fixed during testing:
removeFromParent()was doingstatic_pointer_cast<UIGrid>on a GridView (segfault)find_pathwas fetchingmcrfpy.Grid(now GridView) but casting toPyUIGridObject(segfault)set_gridwas casting Grid (now GridView) toPyUIGridObject(garbage grid dimensions)centeras Vector2f animation property, andshader.*property supportWhat's NOT in this commit