Coverage for cuda/core/texture/_mipmapped_array.pyx: 85.71%
63 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 01:38 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 01:38 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2#
3# SPDX-License-Identifier: Apache-2.0
5from __future__ import annotations
7from libc.string cimport memset
9from cuda.bindings cimport cydriver
10from cuda.core.texture._array cimport _array_from_handle
11from cuda.core.texture._array import ArrayFormat, _validate_array_shape, _validate_format_channels
12from cuda.core._resource_handles cimport (
13 OpaqueArrayHandle,
14 MipmappedArrayHandle,
15 as_intptr,
16 create_array_level_handle,
17 create_mipmapped_array_handle,
18 get_last_error,
19)
20from cuda.core._utils.cuda_utils cimport (
21 HANDLE_RETURN,
22 _get_current_device_id,
23)
26cdef class MipmappedArray:
27 """A mipmapped CUDA array for texture/surface access across levels.
29 Wraps ``CUmipmappedArray``. Each mip level is a distinct, hardware-laid-out
30 allocation accessible only via a :class:`TextureObject` (or by retrieving
31 the level's :class:`OpaqueArray` and binding it as a :class:`SurfaceObject`).
32 Destroying the :class:`MipmappedArray` destroys all level arrays
33 implicitly, so the :class:`OpaqueArray` instances returned by :meth:`get_level`
34 are non-owning and hold a strong reference back to their parent.
36 Construct via :meth:`from_descriptor`.
37 """
39 def __init__(self, *args, **kwargs):
40 raise RuntimeError( 1m
41 "MipmappedArray cannot be instantiated directly. "
42 "Use MipmappedArray.from_descriptor()."
43 )
45 @classmethod
46 def from_descriptor(
47 cls, *, shape, format, num_channels, num_levels, is_surface_load_store=False
48 ):
49 """Allocate a new mipmapped CUDA array.
51 Parameters
52 ----------
53 shape : tuple of int
54 ``(width,)``, ``(width, height)``, or ``(width, height, depth)``
55 in elements, for the base (level 0) mip.
56 format : ArrayFormat
57 Element format.
58 num_channels : int
59 Channels per element. Must be 1, 2, or 4.
60 num_levels : int
61 Number of mip levels to allocate; must be >= 1. The driver caps
62 this at the log2 of the largest dimension; passing a larger value
63 yields a driver error.
64 is_surface_load_store : bool
65 If True, allocate with ``CUDA_ARRAY3D_SURFACE_LDST`` so individual
66 levels (obtained via :meth:`get_level`) can be bound as
67 :class:`SurfaceObject` for kernel-side writes. Default False.
69 Returns
70 -------
71 MipmappedArray
72 """
73 _validate_format_channels(format, num_channels) 1acbdenolkhifg
74 shape_t = _validate_array_shape(shape) 1acbdelkhifg
76 levels = int(num_levels) 1acbdekhifg
77 if levels < 1: 1acbdekhifg
78 raise ValueError(f"num_levels must be >= 1, got {levels}") 1k
80 cdef cydriver.CUarray_format c_format = <cydriver.CUarray_format><int>format 1acbdehifg
81 cdef cydriver.CUDA_ARRAY3D_DESCRIPTOR desc3d
82 cdef int rank = len(shape_t) 1acbdehifg
83 cdef unsigned int flags = (
84 cydriver.CUDA_ARRAY3D_SURFACE_LDST if is_surface_load_store else 0 1acbdehifg
85 )
86 cdef unsigned int c_levels = <unsigned int>levels 1acbdehifg
88 # Mipmap creation uses the 3D descriptor regardless of rank; lower-rank
89 # shapes use Height=0/Depth=0 sentinels, matching cuArray3DCreate.
90 memset(&desc3d, 0, sizeof(desc3d)) 1acbdehifg
91 desc3d.Width = <size_t>shape_t[0] 1acbdehifg
92 desc3d.Height = <size_t>(shape_t[1] if rank >= 2 else 0) 1acbdehifg
93 desc3d.Depth = <size_t>(shape_t[2] if rank >= 3 else 0) 1acbdehifg
94 desc3d.Format = c_format 1acbdehifg
95 desc3d.NumChannels = <unsigned int>num_channels 1acbdehifg
96 desc3d.Flags = flags 1acbdehifg
98 cdef MipmappedArrayHandle h = create_mipmapped_array_handle(desc3d, c_levels) 1acbdehifg
99 if not h: 1acbdehifg
100 HANDLE_RETURN(get_last_error())
102 cdef MipmappedArray self = cls.__new__(cls) 1acbdehifg
103 self._handle = h 1acbdehifg
104 self._shape = shape_t 1acbdehifg
105 self._format = c_format 1acbdehifg
106 self._num_channels = num_channels 1acbdehifg
107 self._num_levels = <unsigned int>levels 1acbdehifg
108 self._surface_load_store = bool(is_surface_load_store) 1acbdehifg
109 self._device_id = _get_current_device_id() 1acbdehifg
110 return self 1acbdehifg
112 def get_level(self, level):
113 """Return a non-owning :class:`OpaqueArray` view of the given mip level.
115 Parameters
116 ----------
117 level : int
118 Mip level index in ``[0, num_levels)``.
120 Returns
121 -------
122 OpaqueArray
123 A non-owning :class:`OpaqueArray` wrapping the level's ``CUarray``.
124 The :class:`MipmappedArray` is kept alive for the lifetime of the
125 returned :class:`OpaqueArray`; the underlying storage is released only
126 when this :class:`MipmappedArray` is destroyed.
127 """
128 lvl = int(level) 1cbde
129 if lvl < 0: 1cbde
130 raise ValueError(f"level must be >= 0, got {lvl}") 1b
131 if lvl >= <int>self._num_levels: 1cbde
132 raise ValueError( 1b
133 f"level ({lvl}) must be < num_levels ({self._num_levels})" 1jb
134 )
136 cdef OpaqueArrayHandle h_level = create_array_level_handle(self._handle, <unsigned int>lvl) 1cde
137 if not h_level: 1cde
138 HANDLE_RETURN(get_last_error())
139 # The returned OpaqueArray is non-owning; its C++ box embeds this mipmap's
140 # handle, so the parent's storage structurally outlives the level view
141 # (no Python parent reference needed).
142 return _array_from_handle(h_level, self._device_id) 1cde
144 @property
145 def handle(self):
146 """The underlying ``CUmipmappedArray`` as an integer."""
147 return as_intptr(self._handle) 1ag
149 @property
150 def shape(self):
151 """Base-level (level 0) allocation shape, in elements."""
152 return self._shape 1a
154 @property
155 def format(self):
156 """The element :class:`ArrayFormat`."""
157 return ArrayFormat(self._format) 1a
159 @property
160 def num_channels(self):
161 """Channels per element (1, 2, or 4)."""
162 return self._num_channels 1a
164 @property
165 def num_levels(self):
166 """Number of mip levels."""
167 return int(self._num_levels) 1abf
169 @property
170 def is_surface_load_store(self):
171 """True if this mipmap (and each of its levels) was created with
172 ``CUDA_ARRAY3D_SURFACE_LDST`` and can back a :class:`SurfaceObject`."""
173 return self._surface_load_store 1a
175 @property
176 def device(self):
177 """The :class:`Device` this mipmap was allocated on."""
178 from cuda.core._device import Device 1a
179 return Device(self._device_id) 1a
181 cpdef close(self):
182 """Release this object's reference to the underlying ``CUmipmappedArray``.
184 Destruction (``cuMipmappedArrayDestroy``) happens via the handle's
185 deleter when the last reference is dropped. A level :class:`OpaqueArray`
186 from :meth:`get_level` holds its own reference to this mipmap's storage,
187 so it stays valid until both it and this object are released. Idempotent.
188 """
189 self._handle.reset() 1acbdhifg
191 def __enter__(self):
192 return self
194 def __exit__(self, exc_type, exc, tb):
195 self.close()
197 def __repr__(self):
198 return (
199 f"MipmappedArray(shape={self._shape}, "
200 f"format={ArrayFormat(self._format).name}, "
201 f"num_channels={self._num_channels}, "
202 f"num_levels={self._num_levels})"
203 )