Implements the #328 bulk-edit convention for grid layers. Writable zero-copy views of layer data are exposed ONLY through a context manager: with layer.edit() as view: # ColorLayer -> (h, w, 4) uint8 np.asarray(view)[...] = ... # TileLayer -> (h, w) int32 (-1 = no tile) # on __exit__ the whole layer is conservatively invalidated (markDirty), # so the edit re-renders. Both layers already stored dense, contiguous, row-major data (std::vector< sf::Color> / std::vector<int>; render caches chunk separately), so the views are true zero-copy aliases. A new internal _LayerEdit type is the buffer exporter (bf_getbuffer) and the context manager (__enter__ returns memoryview(self), __exit__ calls GridLayer::markDirty()); it is not directly instantiable. The conservative whole-layer invalidation on exit means no change-tracking is needed, and it composes with the #351 render early-out (markDirty bumps content_generation). Independent of GridData SoA (#332): these are the render layers, not the logic grid. A contiguous walkable/transparent view now has its storage prerequisite from #332 but remains a separate follow-up. Regression test issue_335_layer_edit_test.py: buffer shape/format/readonly, zero-copy writeback both directions, the -1 tile sentinel, optional numpy aliasing, and a screenshot check that __exit__ actually re-renders. API-surface golden re-baselined (two new edit methods); docs/stubs regenerated; frozen docstring gate 100%. Suite 312/312. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""Regression test for #335 - ColorLayer/TileLayer numpy buffer views via edit().
|
|
|
|
Per the #328 bulk-edit convention, writable zero-copy views of layer data are
|
|
exposed only through `with layer.edit() as view:`. ColorLayer yields an
|
|
(h, w, 4) uint8 view; TileLayer yields an (h, w) int32 view (-1 = no tile).
|
|
Writes alias the layer storage (no copy); on __exit__ the whole layer is
|
|
conservatively invalidated so the edit re-renders.
|
|
|
|
Verifies buffer shape/format, zero-copy writeback both directions, the -1
|
|
sentinel, optional numpy aliasing, and that exiting the context actually
|
|
re-renders (screenshot diff -- the edit->markDirty->#351 early-out loop).
|
|
"""
|
|
import mcrfpy
|
|
from mcrfpy import automation
|
|
import sys
|
|
import os
|
|
import tempfile
|
|
|
|
TMPDIR = tempfile.mkdtemp(prefix="mcrf335_")
|
|
_n = 0
|
|
|
|
|
|
def shot():
|
|
global _n
|
|
p = os.path.join(TMPDIR, "s%d.png" % _n)
|
|
_n += 1
|
|
automation.screenshot(p)
|
|
with open(p, "rb") as f:
|
|
return f.read()
|
|
|
|
|
|
def main():
|
|
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
|
scene = mcrfpy.Scene("t335")
|
|
scene.activate()
|
|
|
|
cl = mcrfpy.ColorLayer(name="fog", z_index=1)
|
|
tl = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=tex)
|
|
grid = mcrfpy.Grid(grid_size=(8, 6), pos=(10, 10), size=(256, 192),
|
|
texture=tex, layers=[tl, cl])
|
|
scene.children.append(grid)
|
|
|
|
# --- ColorLayer: (h, w, 4) uint8, writable, zero-copy both directions ---
|
|
with cl.edit() as view:
|
|
assert view.shape == (6, 8, 4), f"color shape {view.shape}"
|
|
assert view.format == 'B', f"color format {view.format}"
|
|
assert view.readonly is False
|
|
view[1, 2, 0] = 200 # y=1, x=2, red channel
|
|
assert cl.at(2, 1).r == 200, "color view -> layer writeback failed"
|
|
|
|
# layer -> view direction
|
|
cl.set((3, 4), mcrfpy.Color(7, 8, 9, 255)) # x=3, y=4
|
|
with cl.edit() as view:
|
|
assert view[4, 3, 0] == 7 and view[4, 3, 2] == 9, "layer -> view read failed"
|
|
|
|
# --- TileLayer: (h, w) int32, -1 sentinel ---
|
|
with tl.edit() as view:
|
|
assert view.shape == (6, 8), f"tile shape {view.shape}"
|
|
assert view.format == 'i', f"tile format {view.format}"
|
|
view[5, 6] = 42 # y=5, x=6
|
|
view[0, 0] = -1
|
|
assert tl.at(6, 5) == 42, "tile view -> layer writeback failed"
|
|
assert tl.at(0, 0) == -1, "tile -1 sentinel round-trip failed"
|
|
|
|
# --- Optional numpy zero-copy aliasing ---
|
|
try:
|
|
import numpy as np
|
|
with tl.edit() as view:
|
|
a = np.asarray(view)
|
|
assert a.shape == (6, 8) and a.dtype == np.int32, (a.shape, a.dtype)
|
|
a[3, 3] = 17
|
|
assert tl.at(3, 3) == 17, "numpy write did not alias TileLayer"
|
|
print(" numpy zero-copy verified")
|
|
except ImportError:
|
|
print(" numpy not bundled; memoryview path verified")
|
|
|
|
# --- __exit__ invalidation actually re-renders (edit -> markDirty -> #351) ---
|
|
before = shot()
|
|
with cl.edit() as view:
|
|
# paint a visible opaque block through the view
|
|
for y in range(6):
|
|
for x in range(8):
|
|
view[y, x, 0] = 255
|
|
view[y, x, 3] = 255
|
|
after = shot()
|
|
assert before != after, "layer edit did not re-render on __exit__"
|
|
|
|
print("PASS")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|