Coverage for cuda/core/texture/_texture.pyx: 93.29%
283 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.stdint cimport intptr_t
8from libc.string cimport memset
10from cuda.bindings cimport cydriver
11from cuda.core.texture._array cimport OpaqueArray
12from cuda.core.texture._array import ArrayFormat, _FORMAT_ELEM_SIZE, _validate_format_channels
13from cuda.core._memory._buffer cimport Buffer
14from cuda.core.texture._mipmapped_array cimport MipmappedArray
15from cuda.core.texture._mipmapped_array import MipmappedArray as _PyMipmappedArray
16from cuda.core._resource_handles cimport (
17 TexObjectHandle,
18 as_cu,
19 as_intptr,
20 create_tex_object_handle_array,
21 create_tex_object_handle_linear,
22 create_tex_object_handle_mipmap,
23 get_last_error,
24)
25from cuda.core._utils.cuda_utils cimport (
26 HANDLE_RETURN,
27 _get_current_device_id,
28)
30from dataclasses import dataclass
31from enum import IntEnum
34# Driver texture-descriptor flag bits (CU_TRSF_*).
35_TRSF_READ_AS_INTEGER = 0x01
36_TRSF_NORMALIZED_COORDINATES = 0x02
37_TRSF_SRGB = 0x10
38_TRSF_DISABLE_TRILINEAR_OPTIMIZATION = 0x20
39_TRSF_SEAMLESS_CUBEMAP = 0x40
42class AddressMode(IntEnum):
43 """Boundary behavior for out-of-range texture coordinates."""
44 WRAP = cydriver.CU_TR_ADDRESS_MODE_WRAP
45 CLAMP = cydriver.CU_TR_ADDRESS_MODE_CLAMP
46 MIRROR = cydriver.CU_TR_ADDRESS_MODE_MIRROR
47 BORDER = cydriver.CU_TR_ADDRESS_MODE_BORDER
50class FilterMode(IntEnum):
51 """Texel sampling mode."""
52 POINT = cydriver.CU_TR_FILTER_MODE_POINT
53 LINEAR = cydriver.CU_TR_FILTER_MODE_LINEAR
56class ReadMode(IntEnum):
57 """How sampled values are returned to the kernel.
59 - ``ELEMENT_TYPE``: return the raw element value (integer formats stay
60 integer, float stays float).
61 - ``NORMALIZED_FLOAT``: integer formats are promoted to a normalized
62 ``float`` in ``[0, 1]`` (unsigned) or ``[-1, 1]`` (signed).
63 Float formats are unaffected.
64 """
65 ELEMENT_TYPE = 0
66 NORMALIZED_FLOAT = 1
69class ResourceDescriptor:
70 """Describes the memory backing a :class:`TextureObject`.
72 Construct via the ``from_*`` classmethods:
74 - :meth:`from_array` wraps a :class:`OpaqueArray` (works for both
75 :class:`TextureObject` and :class:`SurfaceObject`).
76 - :meth:`from_mipmapped_array` wraps a :class:`MipmappedArray` for mipmapped
77 sampling (texture only, not surface).
78 - :meth:`from_linear` wraps a :class:`Buffer` as a typed 1D fetch. Texture
79 objects built from a linear resource do not support filtering,
80 normalized coordinates, or addressing modes.
81 - :meth:`from_pitch2d` wraps a :class:`Buffer` as a row-pitched 2D image.
82 Supports filtering and 2D addressing, but only 2D access.
84 Linear and pitch2D resources cannot back a :class:`SurfaceObject` — those
85 require an :class:`OpaqueArray` allocated with ``is_surface_load_store=True``.
86 """
88 __slots__ = (
89 "_kind", "_source",
90 "_format", "_num_channels",
91 "_size_bytes",
92 "_width", "_height", "_pitch_bytes",
93 )
95 def __init__(self):
96 raise RuntimeError( 1N
97 "ResourceDescriptor cannot be instantiated directly. "
98 "Use ResourceDescriptor.from_* factories."
99 )
101 @classmethod
102 def from_array(cls, array):
103 """Build a resource descriptor backed by a :class:`OpaqueArray`."""
104 if not isinstance(array, OpaqueArray): 1cpoqrvwxeginjmkuh
105 raise TypeError(f"array must be a OpaqueArray, got {type(array).__name__}")
106 self = cls.__new__(cls) 1cpoqrvwxeginjmkuh
107 self._kind = "array" 1cpoqrvwxeginjmkuh
108 self._source = array 1cpoqrvwxeginjmkuh
109 self._format = None 1cpoqrvwxeginjmkuh
110 self._num_channels = None 1cpoqrvwxeginjmkuh
111 self._size_bytes = None 1cpoqrvwxeginjmkuh
112 self._width = None 1cpoqrvwxeginjmkuh
113 self._height = None 1cpoqrvwxeginjmkuh
114 self._pitch_bytes = None 1cpoqrvwxeginjmkuh
115 return self 1fcpoqrvwxeginjmkuh
117 @classmethod
118 def from_mipmapped_array(cls, mipmapped_array):
119 """Build a resource descriptor backed by a :class:`MipmappedArray`.
121 Suitable for binding to a :class:`TextureObject` for mipmapped
122 sampling. Not valid as a :class:`SurfaceObject` backing: surfaces
123 require a single :class:`OpaqueArray` level (obtain via
124 :meth:`MipmappedArray.get_level`).
125 """
126 if not isinstance(mipmapped_array, _PyMipmappedArray): 1GyAd
127 raise TypeError( 1G
128 f"mipmapped_array must be a MipmappedArray, got " 1G
129 f"{type(mipmapped_array).__name__}" 1G
130 )
131 self = cls.__new__(cls) 1yAd
132 self._kind = "mipmapped_array" 1yAd
133 self._source = mipmapped_array 1fyAd
134 self._format = None 1yAd
135 self._num_channels = None 1yAd
136 self._size_bytes = None 1yAd
137 self._width = None 1yAd
138 self._height = None 1yAd
139 self._pitch_bytes = None 1yAd
140 return self 1yAd
142 @classmethod
143 def from_linear(cls, buffer, *, format, num_channels, size_bytes=None):
144 """Build a resource descriptor for a linear (typed 1D) texture fetch.
146 Parameters
147 ----------
148 buffer : Buffer
149 Device-memory backing. Must remain alive for the lifetime of any
150 :class:`TextureObject` built from this descriptor.
151 format : ArrayFormat
152 Element format.
153 num_channels : int
154 Channels per element. Must be 1, 2, or 4.
155 size_bytes : int, optional
156 Bytes of ``buffer`` to bind. Defaults to ``buffer.size``. Must not
157 exceed it.
159 Notes
160 -----
161 Texture objects built from a linear resource ignore the
162 :class:`TextureDescriptor` addressing/filtering fields — kernels read
163 through a typed 1D fetch with bounds checking only.
164 """
165 if not isinstance(buffer, Buffer): 1sIJBEDtlb
166 raise TypeError(f"buffer must be a Buffer, got {type(buffer).__name__}") 1J
167 _validate_format_channels(format, num_channels) 1sIBEDtlb
169 buf_size = int(buffer.size) 1sBEDtlb
170 elem = _FORMAT_ELEM_SIZE[int(format)] * int(num_channels) 1sBEDtlb
171 if size_bytes is None: 1sBEDtlb
172 size = buf_size 1slb
173 else:
174 size = int(size_bytes) 1BEDt
175 if size > buf_size: 1BEDt
176 raise ValueError( 1E
177 f"size_bytes ({size}) exceeds buffer.size ({buf_size})" 1E
178 )
179 if size < elem: 1sBDtlb
180 raise ValueError( 1D
181 f"size_bytes ({size}) must be at least one element ({elem} bytes)" 1D
182 )
183 if size % elem != 0: 1sBtlb
184 raise ValueError( 1B
185 f"size_bytes ({size}) must be a multiple of element size " 1B
186 f"({elem} bytes for {format.name} x {num_channels})" 1B
187 )
189 self = cls.__new__(cls) 1stlb
190 self._kind = "linear" 1stlb
191 self._source = buffer 1stlb
192 self._format = int(format) 1stlb
193 self._num_channels = int(num_channels) 1stlb
194 self._size_bytes = size 1stlb
195 self._width = None 1stlb
196 self._height = None 1stlb
197 self._pitch_bytes = None 1stlb
198 return self 1stlb
200 @classmethod
201 def from_pitch2d(
202 cls, buffer, *, format, num_channels, width, height, pitch_bytes
203 ):
204 """Build a resource descriptor for a row-pitched 2D image.
206 Parameters
207 ----------
208 buffer : Buffer
209 Device-memory backing. Must remain alive for the lifetime of any
210 :class:`TextureObject` built from this descriptor.
211 format : ArrayFormat
212 Element format.
213 num_channels : int
214 Channels per element. Must be 1, 2, or 4.
215 width : int
216 Image width, in elements.
217 height : int
218 Image height, in rows.
219 pitch_bytes : int
220 Distance between consecutive rows, in bytes. Must be at least
221 ``width * format_size * num_channels`` and meet the driver's
222 ``CU_DEVICE_ATTRIBUTE_TEXTURE_PITCH_ALIGNMENT``.
223 """
224 if not isinstance(buffer, Buffer): 1KLMFzCla
225 raise TypeError(f"buffer must be a Buffer, got {type(buffer).__name__}") 1M
226 _validate_format_channels(format, num_channels) 1KLFzCla
228 w = int(width) 1FzCla
229 h = int(height) 1FzCla
230 p = int(pitch_bytes) 1FzCla
231 if w < 1: 1FzCla
232 raise ValueError(f"width must be >= 1, got {w}") 1F
233 if h < 1: 1FzCla
234 raise ValueError(f"height must be >= 1, got {h}") 1F
235 elem = _FORMAT_ELEM_SIZE[int(format)] * int(num_channels) 1zCla
236 min_pitch = w * elem 1zCla
237 if p < min_pitch: 1zCla
238 raise ValueError( 1C
239 f"pitch_bytes ({p}) must be >= width * element_size ({min_pitch})" 1C
240 )
241 if p * h > int(buffer.size): 1zla
242 raise ValueError( 1z
243 f"pitch_bytes * height ({p * h}) exceeds buffer.size ({int(buffer.size)})" 1z
244 )
246 self = cls.__new__(cls) 1la
247 self._kind = "pitch2d" 1la
248 self._source = buffer 1la
249 self._format = int(format) 1la
250 self._num_channels = int(num_channels) 1la
251 self._size_bytes = None 1la
252 self._width = w 1la
253 self._height = h 1la
254 self._pitch_bytes = p 1la
255 return self 1la
257 @property
258 def kind(self):
259 return self._kind 1cpoqrsyvwlAxebdaginjmkh
261 @property
262 def source(self):
263 return self._source 1cpoqrsyvwxebdaginjmkh
265 @property
266 def format(self):
267 """The element :class:`ArrayFormat` (``None`` for array-backed)."""
268 return None if self._format is None else ArrayFormat(self._format) 1sa
270 @property
271 def num_channels(self):
272 """Channels per element (``None`` for array-backed)."""
273 return self._num_channels 1s
275 @property
276 def size_bytes(self):
277 """Bytes bound for a linear resource (``None`` for other kinds)."""
278 return self._size_bytes
280 @property
281 def width(self):
282 """Pitch2D image width, in elements (``None`` for other kinds)."""
283 return self._width
285 @property
286 def height(self):
287 """Pitch2D image height, in rows (``None`` for other kinds)."""
288 return self._height
290 @property
291 def pitch_bytes(self):
292 """Pitch2D row pitch, in bytes (``None`` for other kinds)."""
293 return self._pitch_bytes
295 def __repr__(self):
296 if self._kind == "linear": 1sa
297 return ( 1s
298 f"ResourceDescriptor(kind='linear', format={self.format.name}, " 1s
299 f"num_channels={self._num_channels}, size_bytes={self._size_bytes})" 1s
300 )
301 if self._kind == "pitch2d": 1a
302 return ( 1a
303 f"ResourceDescriptor(kind='pitch2d', format={self.format.name}, " 1a
304 f"num_channels={self._num_channels}, " 1a
305 f"width={self._width}, height={self._height}, " 1a
306 f"pitch_bytes={self._pitch_bytes})" 1a
307 )
308 return f"ResourceDescriptor(kind={self._kind!r})"
311@dataclass
312class TextureDescriptor:
313 """Sampling state for a :class:`TextureObject` (mirrors ``CUDA_TEXTURE_DESC``).
315 Attributes
316 ----------
317 address_mode : tuple of AddressMode
318 Boundary behavior per axis. May be a single :class:`AddressMode` (applied
319 to all axes) or a tuple of 1-3 entries (one per dimension).
320 filter_mode : FilterMode
321 Texel sampling mode. Default ``POINT``.
322 read_mode : ReadMode
323 How sampled integer values are returned. Default ``ELEMENT_TYPE``.
324 normalized_coords : bool
325 If True, coordinates are in ``[0, 1]`` instead of pixel indices.
326 srgb : bool
327 If True, perform sRGB → linear conversion on read (8-bit formats only).
328 disable_trilinear_optimization : bool
329 If True, request exact trilinear filtering.
330 seamless_cubemap : bool
331 If True, enable seamless cubemap edge filtering.
332 max_anisotropy : int
333 Maximum anisotropy; 0 disables anisotropic filtering.
334 mipmap_filter_mode : FilterMode
335 Filtering between mipmap levels. Default ``POINT``.
336 mipmap_level_bias : float
337 min_mipmap_level_clamp : float
338 max_mipmap_level_clamp : float
339 border_color : tuple of float or None
340 4-tuple used when ``address_mode`` includes ``BORDER``; ``None`` means
341 zero.
342 """
344 address_mode: AddressMode | tuple[AddressMode, ...] = AddressMode.CLAMP
345 filter_mode: FilterMode = FilterMode.POINT
346 read_mode: ReadMode = ReadMode.ELEMENT_TYPE
347 normalized_coords: bool = False
348 srgb: bool = False
349 disable_trilinear_optimization: bool = False
350 seamless_cubemap: bool = False
351 max_anisotropy: int = 0
352 mipmap_filter_mode: FilterMode = FilterMode.POINT
353 mipmap_level_bias: float = 0.0
354 min_mipmap_level_clamp: float = 0.0
355 max_mipmap_level_clamp: float = 0.0
356 border_color: tuple[float, ...] | None = None
359def _normalize_address_modes(address_mode):
360 """Return a 3-tuple of AddressMode values from a scalar or 1-3 tuple."""
361 if isinstance(address_mode, AddressMode): 1cpoqrebdaginjmkh
362 return (address_mode, address_mode, address_mode) 1cebdaginjmkh
363 try: 1cpoqr
364 modes = tuple(address_mode) 1cpoqr
365 except TypeError as e: 1q
366 raise TypeError( 1q
367 "address_mode must be an AddressMode or a tuple of AddressMode"
368 ) from e 1q
369 if not 1 <= len(modes) <= 3: 1cpor
370 raise ValueError( 1pr
371 f"address_mode tuple must have 1-3 entries, got {len(modes)}" 1pr
372 )
373 for i, m in enumerate(modes): 1co
374 if not isinstance(m, AddressMode): 1co
375 raise TypeError( 1o
376 f"address_mode[{i}] must be an AddressMode, got {type(m).__name__}" 1o
377 )
378 # Pad to 3 entries by repeating the last one.
379 padded = list(modes) + [modes[-1]] * (3 - len(modes)) 1c
380 return tuple(padded) 1c
383cdef class TextureObject:
384 """A bindless texture handle for kernel-side sampled reads.
386 Wraps ``cuTexObjectCreate``. The underlying memory resource (e.g. the
387 :class:`OpaqueArray` referenced by the descriptor) is kept alive for the
388 lifetime of this object to prevent dangling handles.
390 Construct via :meth:`from_descriptor`. Passes to kernels as a 64-bit
391 handle (via the ``handle`` property).
392 """
394 def __init__(self, *args, **kwargs):
395 raise RuntimeError( 1O
396 "TextureObject cannot be instantiated directly. "
397 "Use TextureObject.from_descriptor()."
398 )
400 @classmethod
401 def from_descriptor(cls, *, resource, texture_descriptor):
402 """Create a texture object from a resource + sampling descriptor.
404 Parameters
405 ----------
406 resource : ResourceDescriptor
407 texture_descriptor : TextureDescriptor
408 """
409 if not isinstance(resource, ResourceDescriptor): 1cpoqrebdaginjmkHuh
410 raise TypeError( 1H
411 f"resource must be a ResourceDescriptor, got " 1H
412 f"{type(resource).__name__}" 1H
413 )
414 if not isinstance(texture_descriptor, TextureDescriptor): 1cpoqrebdaginjmkuh
415 raise TypeError( 1u
416 f"texture_descriptor must be a TextureDescriptor, got " 1u
417 f"{type(texture_descriptor).__name__}" 1u
418 )
420 cdef cydriver.CUDA_RESOURCE_DESC res_desc
421 cdef cydriver.CUDA_TEXTURE_DESC tex_desc
422 memset(&res_desc, 0, sizeof(res_desc)) 1cpoqrebdaginjmkh
423 memset(&tex_desc, 0, sizeof(tex_desc)) 1cpoqrebdaginjmkh
425 # --- Resource descriptor ---
426 cdef OpaqueArray arr
427 cdef MipmappedArray mip
428 cdef Buffer buf
429 cdef intptr_t devptr
430 if resource.kind == "array": 1cpoqrebdaginjmkh
431 arr = <OpaqueArray>resource.source 1cpoqreginjmkh
432 res_desc.resType = cydriver.CU_RESOURCE_TYPE_ARRAY 1cpoqreginjmkh
433 res_desc.res.array.hArray = as_cu(arr._handle) 1cpoqreginjmkh
434 elif resource.kind == "mipmapped_array": 1bda
435 mip = <MipmappedArray>resource.source 1d
436 res_desc.resType = cydriver.CU_RESOURCE_TYPE_MIPMAPPED_ARRAY 1d
437 res_desc.res.mipmap.hMipmappedArray = as_cu(mip._handle) 1d
438 elif resource.kind == "linear": 1ba
439 buf = <Buffer>resource.source 1b
440 devptr = int(buf.handle) 1b
441 res_desc.resType = cydriver.CU_RESOURCE_TYPE_LINEAR 1b
442 res_desc.res.linear.devPtr = <cydriver.CUdeviceptr>devptr 1b
443 res_desc.res.linear.format = <cydriver.CUarray_format><int>resource._format 1b
444 res_desc.res.linear.numChannels = <unsigned int>resource._num_channels 1b
445 res_desc.res.linear.sizeInBytes = <size_t>resource._size_bytes 1b
446 elif resource.kind == "pitch2d": 1a
447 buf = <Buffer>resource.source 1a
448 devptr = int(buf.handle) 1a
449 res_desc.resType = cydriver.CU_RESOURCE_TYPE_PITCH2D 1a
450 res_desc.res.pitch2D.devPtr = <cydriver.CUdeviceptr>devptr 1a
451 res_desc.res.pitch2D.format = <cydriver.CUarray_format><int>resource._format 1a
452 res_desc.res.pitch2D.numChannels = <unsigned int>resource._num_channels 1a
453 res_desc.res.pitch2D.width = <size_t>resource._width 1a
454 res_desc.res.pitch2D.height = <size_t>resource._height 1a
455 res_desc.res.pitch2D.pitchInBytes = <size_t>resource._pitch_bytes 1a
456 else:
457 raise NotImplementedError(
458 f"ResourceDescriptor kind {resource.kind!r} is not yet supported"
459 )
461 # --- Texture descriptor ---
462 modes = _normalize_address_modes(texture_descriptor.address_mode) 1cpoqrebdaginjmkh
463 tex_desc.addressMode[0] = <cydriver.CUaddress_mode><int>modes[0] 1cebdaginjmkh
464 tex_desc.addressMode[1] = <cydriver.CUaddress_mode><int>modes[1] 1cebdaginjmkh
465 tex_desc.addressMode[2] = <cydriver.CUaddress_mode><int>modes[2] 1cebdaginjmkh
467 if not isinstance(texture_descriptor.filter_mode, FilterMode): 1cebdaginjmkh
468 raise TypeError( 1n
469 f"filter_mode must be a FilterMode, got " 1n
470 f"{type(texture_descriptor.filter_mode).__name__}" 1n
471 )
472 tex_desc.filterMode = <cydriver.CUfilter_mode><int>texture_descriptor.filter_mode 1cebdagijmkh
474 if not isinstance(texture_descriptor.read_mode, ReadMode): 1cebdagijmkh
475 raise TypeError( 1m
476 f"read_mode must be a ReadMode, got " 1m
477 f"{type(texture_descriptor.read_mode).__name__}" 1m
478 )
480 cdef unsigned int flags = 0 1cebdagijkh
481 # CU_TRSF_READ_AS_INTEGER suppresses normalization, so it maps to
482 # ReadMode.ELEMENT_TYPE.
483 if texture_descriptor.read_mode == ReadMode.ELEMENT_TYPE: 1cebdagijkh
484 flags |= _TRSF_READ_AS_INTEGER 1cebdagijkh
485 if texture_descriptor.normalized_coords: 1cebdagijkh
486 flags |= _TRSF_NORMALIZED_COORDINATES 1ed
487 if texture_descriptor.srgb: 1cebdagijkh
488 flags |= _TRSF_SRGB
489 if texture_descriptor.disable_trilinear_optimization: 1cebdagijkh
490 flags |= _TRSF_DISABLE_TRILINEAR_OPTIMIZATION
491 if texture_descriptor.seamless_cubemap: 1cebdagijkh
492 flags |= _TRSF_SEAMLESS_CUBEMAP
493 tex_desc.flags = flags 1cebdagijkh
495 if texture_descriptor.max_anisotropy < 0: 1cebdagijkh
496 raise ValueError("max_anisotropy must be >= 0") 1k
497 tex_desc.maxAnisotropy = <unsigned int>texture_descriptor.max_anisotropy 1cebdagijh
499 if not isinstance(texture_descriptor.mipmap_filter_mode, FilterMode): 1cebdagijh
500 raise TypeError( 1j
501 f"mipmap_filter_mode must be a FilterMode, got " 1j
502 f"{type(texture_descriptor.mipmap_filter_mode).__name__}" 1j
503 )
504 tex_desc.mipmapFilterMode = <cydriver.CUfilter_mode><int>texture_descriptor.mipmap_filter_mode 1cebdagih
505 tex_desc.mipmapLevelBias = <float>texture_descriptor.mipmap_level_bias 1cebdagih
506 tex_desc.minMipmapLevelClamp = <float>texture_descriptor.min_mipmap_level_clamp 1cebdagih
507 tex_desc.maxMipmapLevelClamp = <float>texture_descriptor.max_mipmap_level_clamp 1cebdagih
509 cdef int i
510 if texture_descriptor.border_color is None: 1cebdagih
511 for i in range(4): 1cebdagh
512 tex_desc.borderColor[i] = 0.0 1cebdagh
513 else:
514 bc = tuple(texture_descriptor.border_color) 1i
515 if len(bc) != 4: 1i
516 raise ValueError( 1i
517 f"border_color must have 4 elements, got {len(bc)}" 1i
518 )
519 for i in range(4):
520 tex_desc.borderColor[i] = <float>bc[i]
522 cdef TexObjectHandle h
523 if resource.kind == "array": 1cebdagh
524 h = create_tex_object_handle_array(res_desc, tex_desc, arr._handle) 1cegh
525 elif resource.kind == "mipmapped_array": 1bda
526 h = create_tex_object_handle_mipmap(res_desc, tex_desc, mip._handle) 1d
527 else: # linear or pitch2d — both backed by a device Buffer
528 h = create_tex_object_handle_linear(res_desc, tex_desc, buf._h_ptr) 1ba
529 if not h: 1cebdagh
530 HANDLE_RETURN(get_last_error())
532 cdef TextureObject self = cls.__new__(cls) 1cebdagh
533 self._handle = h 1cebdagh
534 self._source_ref = resource 1cebdagh
535 self._texture_desc = texture_descriptor 1cebdagh
536 self._device_id = _get_current_device_id() 1cebdagh
537 return self 1cebdagh
539 @property
540 def handle(self):
541 """The underlying ``CUtexObject`` as an integer (64-bit kernel arg)."""
542 return as_intptr(self._handle) 1cebdagh
544 @property
545 def resource(self):
546 """The :class:`ResourceDescriptor` this texture was built from."""
547 return self._source_ref 1ebd
549 @property
550 def texture_descriptor(self):
551 """The :class:`TextureDescriptor` this texture was built from."""
552 return self._texture_desc 1e
554 @property
555 def device(self):
556 from cuda.core._device import Device
557 return Device(self._device_id)
559 cpdef close(self):
560 """Release this object's reference to the underlying ``CUtexObject``.
562 Destruction (``cuTexObjectDestroy``) and release of the backing resource
563 happen via the handle's deleter when the last reference is dropped.
564 Idempotent.
565 """
566 self._handle.reset() 1cebdagh
567 self._source_ref = None 1cebdagh
569 def __enter__(self):
570 return self
572 def __exit__(self, exc_type, exc, tb):
573 self.close()
575 def __repr__(self):
576 return f"TextureObject(handle=0x{as_intptr(self._handle):x})"