From b65554a9936edb1bd3306d07befefefeb26393af Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Fri, 27 Feb 2026 07:33:27 -0800 Subject: [PATCH 1/3] removing the _buffer usage example from docstring --- cuda_core/cuda/core/_graphics.pyx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cuda_core/cuda/core/_graphics.pyx b/cuda_core/cuda/core/_graphics.pyx index 4e1620bb2f..302ba6abc7 100644 --- a/cuda_core/cuda/core/_graphics.pyx +++ b/cuda_core/cuda/core/_graphics.pyx @@ -216,13 +216,6 @@ 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 From 3539bd8b319f5ae9d994a3ef89fab444f1684290 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Fri, 27 Feb 2026 08:32:38 -0800 Subject: [PATCH 2/3] fixes --- cuda_core/cuda/core/_graphics.pxd | 6 +- cuda_core/cuda/core/_graphics.pyx | 133 ++++++++++++++---------------- cuda_core/tests/test_graphics.py | 64 +++++++++----- 3 files changed, 110 insertions(+), 93 deletions(-) diff --git a/cuda_core/cuda/core/_graphics.pxd b/cuda_core/cuda/core/_graphics.pxd index 9a8eb84f50..dabce3f860 100644 --- a/cuda_core/cuda/core/_graphics.pxd +++ b/cuda_core/cuda/core/_graphics.pxd @@ -3,12 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 from cuda.core._resource_handles cimport GraphicsResourceHandle +from cuda.core._memory._buffer cimport Buffer -cdef class GraphicsResource: +cdef class GraphicsResource(Buffer): cdef: GraphicsResourceHandle _handle bint _mapped + object _map_stream - cpdef close(self) + cpdef close(self, stream=*) diff --git a/cuda_core/cuda/core/_graphics.pyx b/cuda_core/cuda/core/_graphics.pyx index 302ba6abc7..d0fd21bd4b 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_with_owner, as_cu, as_intptr, ) +from cuda.core._memory._buffer cimport Buffer from cuda.core._stream cimport Stream, Stream_accept from cuda.core._utils.cuda_utils cimport HANDLE_RETURN -from cuda.core._memory import Buffer - __all__ = ['GraphicsResource'] _REGISTER_FLAGS = { @@ -43,47 +43,18 @@ 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: +cdef class GraphicsResource(Buffer): """RAII wrapper for a CUDA graphics resource (``CUgraphicsResource``). A :class:`GraphicsResource` represents an OpenGL buffer or image that has been registered for access by CUDA. This enables zero-copy sharing of GPU data between CUDA compute kernels and graphics renderers. + :class:`GraphicsResource` inherits from :class:`~cuda.core.Buffer`, so when + mapped it can be used directly anywhere a :class:`~cuda.core.Buffer` is + expected. The buffer properties (:attr:`handle`, :attr:`size`) are only + valid while the resource is mapped. + 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 @@ -107,8 +77,8 @@ cdef class GraphicsResource: .. code-block:: python - buf = resource.map(stream=s) - # ... launch kernels using buf ... + resource.map(stream=s) + # ... launch kernels using resource.handle, resource.size ... resource.unmap(stream=s) """ @@ -157,6 +127,7 @@ cdef class GraphicsResource: ) self._handle = create_graphics_resource_handle(resource) self._mapped = False + self._map_stream = None return self @classmethod @@ -202,32 +173,32 @@ cdef class GraphicsResource: ) self._handle = create_graphics_resource_handle(resource) self._mapped = False + self._map_stream = None return self - def map(self, *, stream: Stream | None = None): + def map(self, *, stream: Stream): """Map this graphics resource for CUDA access. - After mapping, a CUDA device pointer into the underlying graphics - memory is available as a :class:`~cuda.core.Buffer`. + After mapping, the CUDA device pointer and size are available via + the inherited :attr:`~cuda.core.Buffer.handle` and + :attr:`~cuda.core.Buffer.size` properties. Can be used as a context manager for automatic unmapping:: with resource.map(stream=s) as buf: + # buf IS the GraphicsResource, which IS-A Buffer # use buf.handle, buf.size, etc. # automatically unmapped here Parameters ---------- - stream : :class:`~cuda.core.Stream`, optional - The CUDA stream on which to perform the mapping. If ``None``, - the default stream (``0``) is used. + stream : :class:`~cuda.core.Stream` + The CUDA stream on which to perform the mapping. 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. + GraphicsResource + Returns ``self`` (which is a :class:`~cuda.core.Buffer`). Raises ------ @@ -241,12 +212,9 @@ cdef class GraphicsResource: if self._mapped: raise RuntimeError("GraphicsResource is already mapped") + cdef Stream s_obj = Stream_accept(stream) 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.CUstream cy_stream = as_cu(s_obj._h_stream) cdef cydriver.CUdeviceptr dev_ptr = 0 cdef size_t size = 0 @@ -258,20 +226,24 @@ cdef class GraphicsResource: cydriver.cuGraphicsResourceGetMappedPointer(&dev_ptr, &size, raw) ) self._mapped = True - buf = Buffer.from_handle(int(dev_ptr), size, owner=self) - return _MappedBufferContext(buf, self, stream) + # Populate Buffer internals with the mapped device pointer + self._h_ptr = deviceptr_create_with_owner(dev_ptr, None) + self._size = size + self._owner = None + self._mem_attrs_inited = False + self._map_stream = stream + return self - def unmap(self, *, stream: Stream | None = None): + def unmap(self, *, stream: Stream): """Unmap this graphics resource, releasing it back to the graphics API. - After unmapping, the :class:`~cuda.core.Buffer` previously returned - by :meth:`map` must not be used. + After unmapping, the buffer properties (:attr:`handle`, :attr:`size`) + are no longer valid. Parameters ---------- - stream : :class:`~cuda.core.Stream`, optional - The CUDA stream on which to perform the unmapping. If ``None``, - the default stream (``0``) is used. + stream : :class:`~cuda.core.Stream` + The CUDA stream on which to perform the unmapping. Raises ------ @@ -285,34 +257,55 @@ cdef class GraphicsResource: if not self._mapped: raise RuntimeError("GraphicsResource is not mapped") + cdef Stream s_obj = Stream_accept(stream) 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) + cdef cydriver.CUstream cy_stream = as_cu(s_obj._h_stream) with nogil: HANDLE_RETURN( cydriver.cuGraphicsUnmapResources(1, &raw, cy_stream) ) self._mapped = False + # Clear Buffer fields + self._h_ptr.reset() + self._size = 0 + self._map_stream = None + + def __enter__(self): + return self - cpdef close(self): + def __exit__(self, exc_type, exc_val, exc_tb): + if self._mapped: + self.unmap(stream=self._map_stream) + return False + + 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. + + Parameters + ---------- + stream : :class:`~cuda.core.Stream`, optional + Accepted for compatibility with :meth:`Buffer.close` but not + used for the graphics unmap/unregister operations. """ cdef cydriver.CUgraphicsResource raw cdef cydriver.CUstream cy_stream if not self._handle: return if self._mapped: - # Best-effort unmap before unregister + # Best-effort unmap before unregister (use stream 0 as fallback) raw = as_cu(self._handle) cy_stream = 0 with nogil: cydriver.cuGraphicsUnmapResources(1, &raw, cy_stream) self._mapped = False self._handle.reset() + # Clear Buffer fields + self._h_ptr.reset() + self._size = 0 + self._map_stream = None @property def is_mapped(self) -> bool: @@ -320,7 +313,7 @@ cdef class GraphicsResource: return self._mapped @property - def handle(self) -> int: + def resource_handle(self) -> int: """The raw ``CUgraphicsResource`` handle as a Python int.""" return as_intptr(self._handle) diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index a0dfc73edc..d52d7bf9e0 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -138,6 +138,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 # --------------------------------------------------------------------------- @@ -188,14 +195,15 @@ class TestFromGLBuffer: 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 != 0 + assert isinstance(resource, Buffer) assert not resource.is_mapped resource.close() def test_register_write_discard(self): with _gl_context_and_buffer() as (gl_buf, nbytes): resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - assert resource.handle != 0 + assert resource.resource_handle != 0 resource.close() def test_close_is_idempotent(self): @@ -214,7 +222,7 @@ class TestFromGLImage: def test_register_image(self): with _gl_context_and_texture() as (tex_id, target): resource = GraphicsResource.from_gl_image(tex_id, target) - assert resource.handle != 0 + assert resource.resource_handle != 0 assert not resource.is_mapped resource.close() @@ -225,22 +233,24 @@ def test_register_image(self): class TestMapUnmap: - def test_map_returns_buffer(self): + def test_map_returns_self(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 mapped is resource assert mapped.size > 0 assert mapped.handle != 0 - resource.unmap() + resource.unmap(stream=stream) 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 @@ -249,8 +259,9 @@ def test_context_manager_unmaps(self): 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,8 +272,9 @@ 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,) @@ -271,9 +283,7 @@ def test_strided_memory_view_from_mapped_buffer(self): 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 @@ -288,39 +298,44 @@ 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.unmap() + resource.map(stream=stream) + resource.unmap(stream=stream) resource.close() def test_unmap_without_map_raises(self): with _gl_context_and_buffer() as (gl_buf, nbytes): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf) with pytest.raises(RuntimeError, match="not mapped"): - resource.unmap() + resource.unmap(stream=stream) resource.close() 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): + stream = _create_stream() resource = GraphicsResource.from_gl_buffer(gl_buf) resource.close() with pytest.raises(RuntimeError, match="has been closed"): - resource.unmap() + resource.unmap(stream=stream) 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() + resource.map(stream=stream) assert resource.is_mapped resource.close() # Should unmap + unregister without error assert not resource.is_mapped @@ -336,7 +351,7 @@ def test_gc_cleanup(self): """Creating and dropping a resource should not leak.""" with _gl_context_and_buffer() as (gl_buf, nbytes): resource = GraphicsResource.from_gl_buffer(gl_buf) - assert resource.handle != 0 + assert resource.resource_handle != 0 del resource gc.collect() # If we get here without a CUDA error, cleanup succeeded. @@ -355,3 +370,10 @@ def test_repr_closed(self): resource.close() r = repr(resource) assert "closed" in r + + def test_isinstance_buffer(self): + """GraphicsResource should be an instance of Buffer.""" + with _gl_context_and_buffer() as (gl_buf, nbytes): + resource = GraphicsResource.from_gl_buffer(gl_buf) + assert isinstance(resource, Buffer) + resource.close() From dbc41f2035e73787ba0f497c551718861d1a1e2f Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Fri, 27 Feb 2026 11:03:05 -0800 Subject: [PATCH 3/3] fixes --- cuda_core/cuda/core/_graphics.pyx | 15 +++++++++++---- cuda_core/tests/test_graphics.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cuda_core/cuda/core/_graphics.pyx b/cuda_core/cuda/core/_graphics.pyx index d0fd21bd4b..3530963fd1 100644 --- a/cuda_core/cuda/core/_graphics.pyx +++ b/cuda_core/cuda/core/_graphics.pyx @@ -67,9 +67,7 @@ cdef class GraphicsResource(Buffer): .. code-block:: python - resource = GraphicsResource.from_gl_buffer(vbo) - - with resource.map(stream=s) as buf: + with GraphicsResource.from_gl_buffer(vbo, stream=s) as buf: view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) # view.ptr is a CUDA device pointer into the GL buffer @@ -89,7 +87,7 @@ cdef class GraphicsResource(Buffer): ) @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 @@ -103,11 +101,18 @@ cdef class GraphicsResource(Buffer): 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 is immediately mapped on this stream + so it can be used directly as a context manager:: + + with GraphicsResource.from_gl_buffer(vbo, stream=s) as buf: + view = StridedMemoryView.from_buffer(buf, shape=(256,), dtype=np.float32) Returns ------- GraphicsResource A new graphics resource wrapping the registered GL buffer. + If *stream* was given, the resource is already mapped. Raises ------ @@ -128,6 +133,8 @@ cdef class GraphicsResource(Buffer): self._handle = create_graphics_resource_handle(resource) self._mapped = False self._map_stream = None + if stream is not None: + self.map(stream=stream) return self @classmethod diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index d52d7bf9e0..fc75015b70 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -281,6 +281,21 @@ def test_strided_memory_view_from_mapped_buffer(self): 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 buf.is_mapped + 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 not buf.is_mapped + buf.close() + def test_map_with_stream(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, nbytes): stream = _create_stream()