feat(discretemap): buffer protocol for zero-copy numpy views; closes #334

DiscreteMap now implements tp_as_buffer/bf_getbuffer, exposing its dense uint8
storage as a 2-D (height, width) C-contiguous writable view. memoryview(dmap)
and np.asarray(dmap) share memory with the map -- no copy -- so per-entity FOV
memory (#294 perspective_map) and other DiscreteMaps can be inspected/edited
with numpy at native speed.

The exporter owns the buffer via its shared_ptr<DiscreteMap>; getbuffer INCREFs
it for the view's lifetime (balanced by the default PyBuffer_Release DECREF, so
no bf_releasebuffer). shape/strides are stored on the object -- dimensions are
immutable after construction, so concurrent views safely share them.

Independent of the GridData SoA work (#332): DiscreteMap already owned a dense
contiguous uint8_t* with no libtcod coupling.

Regression test issue_334_discretemap_buffer_test.py: shape/format/strides,
row-major [x,y]->[y,x] mapping, zero-copy writeback both directions, optional
numpy aliasing (skipped when not bundled), and a buffer over a live entity
perspective_map. Suite 309/309.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-07-11 01:00:53 -04:00
commit 8af3bf45b6
3 changed files with 115 additions and 0 deletions

View file

@ -1317,6 +1317,49 @@ PyObject* PyDiscreteMap::mask(PyDiscreteMapObject* self, PyObject* Py_UNUSED(arg
return PyMemoryView_FromMemory(reinterpret_cast<char*>(self->values), len, PyBUF_WRITE);
}
// ============================================================================
// Buffer protocol (#334) - zero-copy 2D (h, w) uint8 view.
// np.asarray(dmap) -> shape (h, w), dtype uint8, C-contiguous, writable.
// The exporter (this object) owns the buffer via its shared_ptr<DiscreteMap>;
// INCREF keeps it alive for the view's lifetime (balanced by the default
// PyBuffer_Release DECREF -- no bf_releasebuffer needed). shape/strides live on
// the object (dimensions are immutable after construction, so concurrent views
// safely share them).
// ============================================================================
int PyDiscreteMap::getbuffer(PyObject* exporter, Py_buffer* view, int flags)
{
PyDiscreteMapObject* self = reinterpret_cast<PyDiscreteMapObject*>(exporter);
if (!self->values) {
PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized");
view->obj = nullptr;
return -1;
}
self->buf_shape[0] = self->h; // rows
self->buf_shape[1] = self->w; // cols
self->buf_strides[0] = self->w; // itemsize == 1, so byte strides == element strides
self->buf_strides[1] = 1;
view->buf = self->values;
view->obj = exporter;
Py_INCREF(exporter);
view->len = static_cast<Py_ssize_t>(self->w) * static_cast<Py_ssize_t>(self->h);
view->readonly = 0;
view->itemsize = 1;
view->format = (flags & PyBUF_FORMAT) ? const_cast<char*>("B") : nullptr;
view->ndim = 2;
view->shape = self->buf_shape;
view->strides = (flags & PyBUF_STRIDES) ? self->buf_strides : nullptr;
view->suboffsets = nullptr;
view->internal = nullptr;
return 0;
}
PyBufferProcs PyDiscreteMap::as_buffer = {
.bf_getbuffer = PyDiscreteMap::getbuffer,
.bf_releasebuffer = nullptr,
};
// ============================================================================
// Serialization
// ============================================================================