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

1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4  

5from __future__ import annotations 

6  

7from libc.string cimport memset 

8  

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) 

24  

25  

26cdef class MipmappedArray: 

27 """A mipmapped CUDA array for texture/surface access across levels. 

28  

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. 

35  

36 Construct via :meth:`from_descriptor`. 

37 """ 

38  

39 def __init__(self, *args, **kwargs): 

40 raise RuntimeError( 1m

41 "MipmappedArray cannot be instantiated directly. " 

42 "Use MipmappedArray.from_descriptor()." 

43 ) 

44  

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. 

50  

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. 

68  

69 Returns 

70 ------- 

71 MipmappedArray 

72 """ 

73 _validate_format_channels(format, num_channels) 1acbdenolkhifg

74 shape_t = _validate_array_shape(shape) 1acbdelkhifg

75  

76 levels = int(num_levels) 1acbdekhifg

77 if levels < 1: 1acbdekhifg

78 raise ValueError(f"num_levels must be >= 1, got {levels}") 1k

79  

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

87  

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

97  

98 cdef MipmappedArrayHandle h = create_mipmapped_array_handle(desc3d, c_levels) 1acbdehifg

99 if not h: 1acbdehifg

100 HANDLE_RETURN(get_last_error()) 

101  

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

111  

112 def get_level(self, level): 

113 """Return a non-owning :class:`OpaqueArray` view of the given mip level. 

114  

115 Parameters 

116 ---------- 

117 level : int 

118 Mip level index in ``[0, num_levels)``. 

119  

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 ) 

135  

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

143  

144 @property 

145 def handle(self): 

146 """The underlying ``CUmipmappedArray`` as an integer.""" 

147 return as_intptr(self._handle) 1ag

148  

149 @property 

150 def shape(self): 

151 """Base-level (level 0) allocation shape, in elements.""" 

152 return self._shape 1a

153  

154 @property 

155 def format(self): 

156 """The element :class:`ArrayFormat`.""" 

157 return ArrayFormat(self._format) 1a

158  

159 @property 

160 def num_channels(self): 

161 """Channels per element (1, 2, or 4).""" 

162 return self._num_channels 1a

163  

164 @property 

165 def num_levels(self): 

166 """Number of mip levels.""" 

167 return int(self._num_levels) 1abf

168  

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

174  

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

180  

181 cpdef close(self): 

182 """Release this object's reference to the underlying ``CUmipmappedArray``. 

183  

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

190  

191 def __enter__(self): 

192 return self 

193  

194 def __exit__(self, exc_type, exc, tb): 

195 self.close() 

196  

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 )