diff --git a/cuda_core/cuda/core/_cpp/resource_handles.cpp b/cuda_core/cuda/core/_cpp/resource_handles.cpp index 033fa603e7..c9315424c2 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.cpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.cpp @@ -57,6 +57,7 @@ decltype(&cuLibraryUnload) p_cuLibraryUnload = nullptr; decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel = nullptr; // GL interop pointers +decltype(&cuGraphicsUnmapResources) p_cuGraphicsUnmapResources = nullptr; decltype(&cuGraphicsUnregisterResource) p_cuGraphicsUnregisterResource = nullptr; // NVRTC function pointers @@ -569,6 +570,23 @@ DevicePtrHandle deviceptr_create_with_owner(CUdeviceptr ptr, PyObject* owner) { return DevicePtrHandle(box, &box->resource); } +DevicePtrHandle deviceptr_create_mapped_graphics( + CUdeviceptr ptr, + const GraphicsResourceHandle& h_resource, + const StreamHandle& h_stream +) { + auto box = std::shared_ptr( + new DevicePtrBox{ptr, h_stream}, + [h_resource](DevicePtrBox* b) { + GILReleaseGuard gil; + CUgraphicsResource resource = as_cu(h_resource); + p_cuGraphicsUnmapResources(1, &resource, as_cu(b->h_stream)); + delete b; + } + ); + return DevicePtrHandle(box, &box->resource); +} + // ============================================================================ // MemoryResource-owned Device Pointer Handles // ============================================================================ diff --git a/cuda_core/cuda/core/_cpp/resource_handles.hpp b/cuda_core/cuda/core/_cpp/resource_handles.hpp index d91f999ac6..bb408ced05 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.hpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.hpp @@ -73,6 +73,7 @@ extern decltype(&cuLibraryUnload) p_cuLibraryUnload; extern decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel; // Graphics interop +extern decltype(&cuGraphicsUnmapResources) p_cuGraphicsUnmapResources; extern decltype(&cuGraphicsUnregisterResource) p_cuGraphicsUnregisterResource; // ============================================================================ @@ -244,6 +245,16 @@ DevicePtrHandle deviceptr_create_ref(CUdeviceptr ptr); // If owner is nullptr, equivalent to deviceptr_create_ref. DevicePtrHandle deviceptr_create_with_owner(CUdeviceptr ptr, PyObject* owner); +// Create a device pointer handle for a mapped graphics resource. +// The pointer structurally depends on the provided graphics resource handle. +// When the last reference is released, cuGraphicsUnmapResources is called on +// the stored stream, then the graphics resource may be unregistered when its +// own handle is released. +DevicePtrHandle deviceptr_create_mapped_graphics( + CUdeviceptr ptr, + const GraphicsResourceHandle& h_resource, + const StreamHandle& h_stream); + // Callback type for MemoryResource deallocation. // Called from the shared_ptr deleter when a handle created via // deviceptr_create_with_mr is destroyed. The implementation is responsible diff --git a/cuda_core/cuda/core/_graphics.pxd b/cuda_core/cuda/core/_graphics.pxd index 9a8eb84f50..520a366bbd 100644 --- a/cuda_core/cuda/core/_graphics.pxd +++ b/cuda_core/cuda/core/_graphics.pxd @@ -9,6 +9,8 @@ cdef class GraphicsResource: cdef: GraphicsResourceHandle _handle - bint _mapped + object _mapped_buffer + object _context_manager_stream + object _entered_buffer - cpdef close(self) + cpdef close(self, stream=*) diff --git a/cuda_core/cuda/core/_graphics.pyx b/cuda_core/cuda/core/_graphics.pyx index 4e1620bb2f..fc053da8cb 100644 --- a/cuda_core/cuda/core/_graphics.pyx +++ b/cuda_core/cuda/core/_graphics.pyx @@ -7,14 +7,14 @@ from __future__ import annotations from cuda.bindings cimport cydriver from cuda.core._resource_handles cimport ( create_graphics_resource_handle, + deviceptr_create_mapped_graphics, as_cu, as_intptr, ) -from cuda.core._stream cimport Stream, Stream_accept +from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle +from cuda.core._stream cimport Stream, Stream_accept, default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN -from cuda.core._memory import Buffer - __all__ = ['GraphicsResource'] _REGISTER_FLAGS = { @@ -43,40 +43,6 @@ def _parse_register_flags(flags): return result -class _MappedBufferContext: - """Context manager returned by :meth:`GraphicsResource.map`. - - Wraps a :class:`~cuda.core.Buffer` and ensures the graphics resource - is unmapped when the context exits. Can also be used without ``with`` - by calling :meth:`GraphicsResource.unmap` explicitly. - """ - __slots__ = ('_buffer', '_resource', '_stream') - - def __init__(self, buffer, resource, stream): - self._buffer = buffer - self._resource = resource - self._stream = stream - - def __enter__(self): - return self._buffer - - def __exit__(self, exc_type, exc_val, exc_tb): - self._resource.unmap(stream=self._stream) - return False - - # Delegate Buffer attributes so the return value of map() is directly usable - @property - def handle(self): - return self._buffer.handle - - @property - def size(self): - return self._buffer.size - - def __repr__(self): - return repr(self._buffer) - - cdef class GraphicsResource: """RAII wrapper for a CUDA graphics resource (``CUgraphicsResource``). @@ -84,6 +50,11 @@ cdef class GraphicsResource: been registered for access by CUDA. This enables zero-copy sharing of GPU data between CUDA compute kernels and graphics renderers. + Mapping the resource returns a :class:`~cuda.core.Buffer` whose lifetime + controls when the graphics resource is unmapped. This keeps stream-ordered + cleanup tied to the mapped pointer itself rather than to mutable state on + the :class:`GraphicsResource` object. + The resource is automatically unregistered when :meth:`close` is called or when the object is garbage collected. @@ -92,8 +63,7 @@ cdef class GraphicsResource: Examples -------- - Register an OpenGL VBO, map it to get a :class:`~cuda.core.Buffer`, and - write to it from CUDA: + Register an OpenGL VBO, map it to get a buffer, and write to it from CUDA: .. code-block:: python @@ -103,13 +73,14 @@ cdef class GraphicsResource: view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) # view.ptr is a CUDA device pointer into the GL buffer - Or use explicit map/unmap for render loops: + Or scope registration separately from mapping: .. code-block:: python - buf = resource.map(stream=s) - # ... launch kernels using buf ... - resource.unmap(stream=s) + with GraphicsResource.from_gl_buffer(vbo) as resource: + with resource.map(stream=s) as buf: + # ... launch kernels using buf.handle, buf.size ... + pass """ def __init__(self): @@ -119,7 +90,7 @@ cdef class GraphicsResource: ) @classmethod - def from_gl_buffer(cls, int gl_buffer, *, flags=None) -> GraphicsResource: + def from_gl_buffer(cls, int gl_buffer, *, flags=None, stream=None) -> GraphicsResource: """Register an OpenGL buffer object for CUDA access. Parameters @@ -133,11 +104,28 @@ cdef class GraphicsResource: Multiple flags can be combined by passing a sequence (e.g., ``("surface_load_store", "read_only")``). Defaults to ``None`` (no flags). + stream : :class:`~cuda.core.Stream`, optional + If provided, the resource can be used directly as a context manager + and it will be mapped on entry:: + + with GraphicsResource.from_gl_buffer(vbo, stream=s) as buf: + view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) + + If omitted, the returned resource can still be used as a context + manager to scope registration and automatic cleanup:: + + with GraphicsResource.from_gl_buffer(vbo) as resource: + with resource.map(stream=s) as buf: + ... Returns ------- GraphicsResource A new graphics resource wrapping the registered GL buffer. + The returned resource can be used as a context manager. If + *stream* was given, entering maps the resource and yields a + :class:`~cuda.core.Buffer`; otherwise entering yields the + :class:`GraphicsResource` itself and closes it on exit. Raises ------ @@ -156,7 +144,9 @@ cdef class GraphicsResource: cydriver.cuGraphicsGLRegisterBuffer(&resource, cy_buffer, cy_flags) ) self._handle = create_graphics_resource_handle(resource) - self._mapped = False + self._mapped_buffer = None + self._context_manager_stream = stream + self._entered_buffer = None return self @classmethod @@ -201,10 +191,22 @@ cdef class GraphicsResource: cydriver.cuGraphicsGLRegisterImage(&resource, cy_image, cy_target, cy_flags) ) self._handle = create_graphics_resource_handle(resource) - self._mapped = False + self._mapped_buffer = None + self._context_manager_stream = None + self._entered_buffer = None return self - def map(self, *, stream: Stream | None = None): + def _get_mapped_buffer(self): + cdef Buffer buf + if self._mapped_buffer is None: + return None + buf = self._mapped_buffer + if not buf._h_ptr: + self._mapped_buffer = None + return None + return self._mapped_buffer + + def map(self, *, stream: Stream | None = None) -> Buffer: """Map this graphics resource for CUDA access. After mapping, a CUDA device pointer into the underlying graphics @@ -216,25 +218,17 @@ cdef class GraphicsResource: # use buf.handle, buf.size, etc. # automatically unmapped here - Or called directly for explicit control:: - - mapped = resource.map(stream=s) - buf = mapped._buffer # or use mapped.handle, mapped.size - # ... do work ... - resource.unmap(stream=s) - Parameters ---------- stream : :class:`~cuda.core.Stream`, optional The CUDA stream on which to perform the mapping. If ``None``, - the default stream (``0``) is used. + the current default stream is used. Returns ------- - _MappedBufferContext - An object that is both a context manager and provides access - to the underlying :class:`~cuda.core.Buffer`. When used with - ``with``, the resource is unmapped on exit. + Buffer + A buffer whose lifetime controls when the graphics resource is + unmapped. Raises ------ @@ -243,20 +237,20 @@ cdef class GraphicsResource: CUDAError If the mapping fails. """ + cdef Stream s_obj + cdef cydriver.CUgraphicsResource raw + cdef cydriver.CUstream cy_stream + cdef cydriver.CUdeviceptr dev_ptr = 0 + cdef size_t size = 0 + cdef Buffer buf if not self._handle: raise RuntimeError("GraphicsResource has been closed") - if self._mapped: + if self._get_mapped_buffer() is not None: raise RuntimeError("GraphicsResource is already mapped") - cdef cydriver.CUgraphicsResource raw = as_cu(self._handle) - cdef cydriver.CUstream cy_stream = 0 - cdef Stream s_obj = None - if stream is not None: - s_obj = Stream_accept(stream) - cy_stream = as_cu(s_obj._h_stream) - - cdef cydriver.CUdeviceptr dev_ptr = 0 - cdef size_t size = 0 + s_obj = default_stream() if stream is None else Stream_accept(stream) + raw = as_cu(self._handle) + cy_stream = as_cu(s_obj._h_stream) with nogil: HANDLE_RETURN( cydriver.cuGraphicsMapResources(1, &raw, cy_stream) @@ -264,9 +258,14 @@ cdef class GraphicsResource: HANDLE_RETURN( cydriver.cuGraphicsResourceGetMappedPointer(&dev_ptr, &size, raw) ) - self._mapped = True - buf = Buffer.from_handle(int(dev_ptr), size, owner=self) - return _MappedBufferContext(buf, self, stream) + buf = Buffer_from_deviceptr_handle( + deviceptr_create_mapped_graphics(dev_ptr, self._handle, s_obj._h_stream), + size, + None, + None, + ) + self._mapped_buffer = buf + return buf def unmap(self, *, stream: Stream | None = None): """Unmap this graphics resource, releasing it back to the graphics API. @@ -277,8 +276,8 @@ cdef class GraphicsResource: Parameters ---------- stream : :class:`~cuda.core.Stream`, optional - The CUDA stream on which to perform the unmapping. If ``None``, - the default stream (``0``) is used. + If provided, overrides the stream that will be used when the + mapped buffer is closed. Otherwise the mapping stream is reused. Raises ------ @@ -287,51 +286,68 @@ cdef class GraphicsResource: CUDAError If the unmapping fails. """ + cdef object buf_obj + cdef Buffer buf if not self._handle: raise RuntimeError("GraphicsResource has been closed") - if not self._mapped: + buf_obj = self._get_mapped_buffer() + if buf_obj is None: raise RuntimeError("GraphicsResource is not mapped") + buf = buf_obj + buf.close(stream=stream) + self._mapped_buffer = None - cdef cydriver.CUgraphicsResource raw = as_cu(self._handle) - cdef cydriver.CUstream cy_stream = 0 - if stream is not None: - cy_stream = as_cu((Stream_accept(stream))._h_stream) - with nogil: - HANDLE_RETURN( - cydriver.cuGraphicsUnmapResources(1, &raw, cy_stream) - ) - self._mapped = False + def __enter__(self): + if self._context_manager_stream is None: + return self + self._entered_buffer = self.map(stream=self._context_manager_stream) + return self._entered_buffer + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False - cpdef close(self): + cpdef close(self, stream=None): """Unregister this graphics resource from CUDA. - If the resource is currently mapped, it is unmapped first (on the - default stream). After closing, the resource cannot be used again. + If the resource is currently mapped, it is unmapped first. After + closing, the resource cannot be used again. + + Parameters + ---------- + stream : :class:`~cuda.core.Stream`, optional + Optional override for the stream used to close the currently + mapped buffer, if one exists. """ - cdef cydriver.CUgraphicsResource raw - cdef cydriver.CUstream cy_stream + cdef object buf_obj + cdef Buffer buf if not self._handle: return - if self._mapped: - # Best-effort unmap before unregister - raw = as_cu(self._handle) - cy_stream = 0 - with nogil: - cydriver.cuGraphicsUnmapResources(1, &raw, cy_stream) - self._mapped = False + buf_obj = self._get_mapped_buffer() + if buf_obj is not None: + buf = buf_obj + buf.close(stream=stream) + self._mapped_buffer = None self._handle.reset() + self._context_manager_stream = None + self._entered_buffer = None @property def is_mapped(self) -> bool: """Whether the resource is currently mapped for CUDA access.""" - return self._mapped + return self._get_mapped_buffer() is not None @property def handle(self) -> int: """The raw ``CUgraphicsResource`` handle as a Python int.""" return as_intptr(self._handle) + @property + def resource_handle(self) -> int: + """Alias for :attr:`handle`.""" + return self.handle + def __repr__(self): - mapped_str = " mapped" if self._mapped else "" + mapped_str = " mapped" if self.is_mapped else "" closed_str = " closed" if not self._handle else "" return f"" diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index 83009f74ae..0c0f0800f1 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -189,6 +189,13 @@ cdef class Buffer: """ Buffer_close(self, stream) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + def copy_to(self, dst: Buffer = None, *, stream: Stream | GraphBuilder) -> Buffer: """Copy from this buffer to the dst buffer asynchronously on the given stream. diff --git a/cuda_core/cuda/core/_resource_handles.pxd b/cuda_core/cuda/core/_resource_handles.pxd index 7a53a4f25f..562395b1db 100644 --- a/cuda_core/cuda/core/_resource_handles.pxd +++ b/cuda_core/cuda/core/_resource_handles.pxd @@ -112,6 +112,10 @@ cdef DevicePtrHandle deviceptr_alloc(size_t size) except+ nogil cdef DevicePtrHandle deviceptr_alloc_host(size_t size) except+ nogil cdef DevicePtrHandle deviceptr_create_ref(cydriver.CUdeviceptr ptr) except+ nogil cdef DevicePtrHandle deviceptr_create_with_owner(cydriver.CUdeviceptr ptr, object owner) except+ nogil +cdef DevicePtrHandle deviceptr_create_mapped_graphics( + cydriver.CUdeviceptr ptr, + const GraphicsResourceHandle& h_resource, + const StreamHandle& h_stream) except+ nogil cdef DevicePtrHandle deviceptr_create_with_mr( cydriver.CUdeviceptr ptr, size_t size, object mr) except+ nogil diff --git a/cuda_core/cuda/core/_resource_handles.pyx b/cuda_core/cuda/core/_resource_handles.pyx index 47d0a86d04..57143c2d30 100644 --- a/cuda_core/cuda/core/_resource_handles.pyx +++ b/cuda_core/cuda/core/_resource_handles.pyx @@ -93,6 +93,10 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": cydriver.CUdeviceptr ptr) except+ nogil DevicePtrHandle deviceptr_create_with_owner "cuda_core::deviceptr_create_with_owner" ( cydriver.CUdeviceptr ptr, object owner) except+ nogil + DevicePtrHandle deviceptr_create_mapped_graphics "cuda_core::deviceptr_create_mapped_graphics" ( + cydriver.CUdeviceptr ptr, + const GraphicsResourceHandle& h_resource, + const StreamHandle& h_stream) except+ nogil # MR deallocation callback ctypedef void (*MRDeallocCallback)( @@ -208,6 +212,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": void* p_cuLibraryGetKernel "reinterpret_cast(cuda_core::p_cuLibraryGetKernel)" # Graphics interop + void* p_cuGraphicsUnmapResources "reinterpret_cast(cuda_core::p_cuGraphicsUnmapResources)" void* p_cuGraphicsUnregisterResource "reinterpret_cast(cuda_core::p_cuGraphicsUnregisterResource)" # NVRTC @@ -267,6 +272,7 @@ p_cuLibraryUnload = _get_driver_fn("cuLibraryUnload") p_cuLibraryGetKernel = _get_driver_fn("cuLibraryGetKernel") # Graphics interop +p_cuGraphicsUnmapResources = _get_driver_fn("cuGraphicsUnmapResources") p_cuGraphicsUnregisterResource = _get_driver_fn("cuGraphicsUnregisterResource") # ============================================================================= diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index a0dfc73edc..cfa5a8541b 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -8,6 +8,7 @@ import gc import os import sys +from unittest.mock import patch import numpy as np import pytest @@ -138,6 +139,13 @@ def _gl_context_and_texture(width=16, height=16): pass +def _create_stream(): + """Create a CUDA stream for testing.""" + dev = Device(0) + dev.set_current() + return dev.create_stream() + + # --------------------------------------------------------------------------- # Register flags parsing tests # --------------------------------------------------------------------------- @@ -189,6 +197,8 @@ def test_register_default_flags(self): with _gl_context_and_buffer() as (gl_buf, nbytes): resource = GraphicsResource.from_gl_buffer(gl_buf) assert resource.handle != 0 + assert resource.resource_handle == resource.handle + assert not isinstance(resource, Buffer) assert not resource.is_mapped resource.close() @@ -227,30 +237,38 @@ def test_register_image(self): class TestMapUnmap: def test_map_returns_buffer(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - mapped = resource.map() + mapped = resource.map(stream=stream) assert resource.is_mapped - # mapped is a _MappedBufferContext; its .handle and .size delegate to Buffer + assert isinstance(mapped, Buffer) + assert mapped is not resource assert mapped.size > 0 assert mapped.handle != 0 - resource.unmap() + assert resource.handle != mapped.handle + resource.unmap(stream=stream) + assert mapped.handle == 0 assert not resource.is_mapped resource.close() def test_context_manager_unmaps(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - with resource.map() as buf: + with resource.map(stream=stream) as buf: assert isinstance(buf, Buffer) assert resource.is_mapped assert buf.size > 0 + assert buf.handle != 0 + assert buf.handle == 0 assert not resource.is_mapped resource.close() def test_context_manager_unmaps_on_exception(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - with pytest.raises(ValueError, match="test error"), resource.map() as _buf: + with pytest.raises(ValueError, match="test error"), resource.map(stream=stream) as _buf: assert resource.is_mapped raise ValueError("test error") # Must be unmapped even after exception @@ -261,24 +279,72 @@ def test_strided_memory_view_from_mapped_buffer(self): """End-to-end: register, map, create StridedMemoryView.""" nbytes = 256 * 4 # 256 float32 elements with _gl_context_and_buffer(nbytes=nbytes) as (gl_buf, _): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - with resource.map() as buf: + with resource.map(stream=stream) as buf: view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) assert view.ptr == int(buf.handle) assert view.shape == (256,) assert view.is_device_accessible resource.close() + def test_from_gl_buffer_with_stream_context_manager(self): + """Register + auto-map via from_gl_buffer(stream=), then create StridedMemoryView.""" + nbytes = 256 * 4 # 256 float32 elements + with _gl_context_and_buffer(nbytes=nbytes) as (gl_buf, _): + stream = _create_stream() + with GraphicsResource.from_gl_buffer(gl_buf, stream=stream) as buf: + assert isinstance(buf, Buffer) + assert buf.size == nbytes + view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) + assert view.ptr == int(buf.handle) + assert view.shape == (256,) + assert view.is_device_accessible + assert buf.handle == 0 + assert buf.size == 0 + + def test_resource_context_manager_auto_closes(self): + with _gl_context_and_buffer(nbytes=4096) as (gl_buf, _): + with GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") as resource: + assert isinstance(resource, GraphicsResource) + assert resource.handle != 0 + assert not resource.is_mapped + assert resource.handle == 0 + + def test_resource_context_manager_can_map_inside_scope(self): + with _gl_context_and_buffer(nbytes=4096) as (gl_buf, _): + stream = _create_stream() + with GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard").map(stream=stream) as buf: + assert isinstance(buf, Buffer) + assert buf.handle != 0 + + def test_chained_map_context_manager_unmaps(self): + with _gl_context_and_buffer(nbytes=4096) as (gl_buf, _): + stream = _create_stream() + with GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard").map(stream=stream) as buf: + assert isinstance(buf, Buffer) + assert buf.handle != 0 + assert buf.size > 0 + assert buf.handle == 0 + assert buf.size == 0 + def test_map_with_stream(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, nbytes): - dev = Device(0) - dev.set_current() - stream = dev.create_stream() + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") with resource.map(stream=stream) as buf: assert buf.size > 0 resource.close() + def test_map_with_default_stream(self): + with _gl_context_and_buffer(nbytes=4096) as (gl_buf, _): + resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") + with resource.map() as buf: + assert isinstance(buf, Buffer) + assert buf.size > 0 + assert not resource.is_mapped + resource.close() + # --------------------------------------------------------------------------- # Error handling tests @@ -288,10 +354,11 @@ def test_map_with_stream(self): class TestErrorHandling: def test_double_map_raises(self): with _gl_context_and_buffer() as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf) - resource.map() + resource.map(stream=stream) with pytest.raises(RuntimeError, match="already mapped"): - resource.map() + resource.map(stream=stream) resource.unmap() resource.close() @@ -304,10 +371,11 @@ def test_unmap_without_map_raises(self): def test_map_after_close_raises(self): with _gl_context_and_buffer() as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf) resource.close() with pytest.raises(RuntimeError, match="has been closed"): - resource.map() + resource.map(stream=stream) def test_unmap_after_close_raises(self): with _gl_context_and_buffer() as (gl_buf, nbytes): @@ -319,11 +387,43 @@ def test_unmap_after_close_raises(self): def test_close_while_mapped(self): """close() should unmap before unregistering.""" with _gl_context_and_buffer() as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - resource.map() + buf = resource.map(stream=stream) assert resource.is_mapped resource.close() # Should unmap + unregister without error assert not resource.is_mapped + assert buf.handle == 0 + + def test_close_while_mapped_passes_stream_override(self): + with _gl_context_and_buffer() as (gl_buf, _): + map_stream = _create_stream() + close_stream = _create_stream() + resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") + resource.map(stream=map_stream) + + original_close = Buffer.close + + def tracking_close(self, stream=None): + tracking_close.calls.append(stream) + return original_close(self, stream=stream) + + tracking_close.calls = [] + + with patch.object(Buffer, "close", new=tracking_close): + resource.close(stream=close_stream) + + assert tracking_close.calls == [close_stream] + assert not resource.is_mapped + + def test_buffer_close_updates_resource_state(self): + with _gl_context_and_buffer() as (gl_buf, _): + stream = _create_stream() + resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") + buf = resource.map(stream=stream) + assert resource.is_mapped + buf.close() + assert not resource.is_mapped # --------------------------------------------------------------------------- @@ -355,3 +455,9 @@ def test_repr_closed(self): resource.close() r = repr(resource) assert "closed" in r + + def test_graphics_resource_is_not_a_buffer(self): + with _gl_context_and_buffer() as (gl_buf, nbytes): + resource = GraphicsResource.from_gl_buffer(gl_buf) + assert not isinstance(resource, Buffer) + resource.close()