Skip to main content

maplibre_native_ffi/
render.rs

1use std::cell::Cell;
2use std::fmt;
3use std::marker::PhantomData;
4use std::mem;
5use std::rc::Rc;
6
7pub use maplibre_core::{PremultipliedRgba8Image, TextureImageInfo};
8use maplibre_native_ffi_core as maplibre_core;
9use maplibre_native_ffi_core::{OpenGLClientApi, OpenGLContextOwnership, RenderResult};
10use maplibre_native_ffi_sys as sys;
11
12use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle};
13use crate::map::MapAttachRef;
14use crate::{HandleOperationError, Result};
15
16/// Borrowed opaque native address used for backend interop handles. It does not
17/// own, retain, dereference, or validate the pointed-to object, and passing it
18/// to MapLibre Native transfers no ownership.
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
20pub struct NativePointer {
21    address: usize,
22    _thread_affine: PhantomData<Rc<()>>,
23}
24
25impl NativePointer {
26    /// Null backend handle value.
27    pub const NULL: Self = Self {
28        address: 0,
29        _thread_affine: PhantomData,
30    };
31
32    /// Creates an opaque borrowed pointer value from a native address.
33    ///
34    /// # Safety
35    ///
36    /// The address must have the correct backend-native type for every API it
37    /// is passed to, and the native object must stay valid for the whole borrow
38    /// that API requires. This wrapper validates nothing.
39    pub unsafe fn from_address(address: usize) -> Self {
40        if address == 0 {
41            Self::NULL
42        } else {
43            Self {
44                address,
45                _thread_affine: PhantomData,
46            }
47        }
48    }
49
50    /// Creates an opaque borrowed pointer value from a raw pointer.
51    ///
52    /// # Safety
53    ///
54    /// The pointer must satisfy the same requirements as
55    /// [`NativePointer::from_address`].
56    pub unsafe fn from_ptr<T>(ptr: *mut T) -> Self {
57        // SAFETY: The caller upholds the native pointer lifetime and type
58        // requirements documented above; this conversion only stores the address.
59        unsafe { Self::from_address(ptr as usize) }
60    }
61
62    /// Returns this opaque value as an integer address.
63    pub fn address(self) -> usize {
64        self.address
65    }
66
67    /// Returns whether this value is null.
68    pub fn is_null(self) -> bool {
69        self.address == 0
70    }
71
72    /// Reconstructs a raw pointer for a backend interop call.
73    ///
74    /// # Safety
75    ///
76    /// The caller must choose the correct pointer type and uphold the lifetime,
77    /// thread-affinity, synchronization, and aliasing requirements of the
78    /// backend API that will receive the pointer.
79    pub unsafe fn as_ptr<T>(self) -> *mut T {
80        self.address as *mut T
81    }
82
83    fn as_void_ptr(self) -> *mut std::ffi::c_void {
84        self.address as *mut std::ffi::c_void
85    }
86}
87
88impl fmt::Debug for NativePointer {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f, "NativePointer(0x{:x})", self.address)
91    }
92}
93
94/// Bit pattern of a borrowed Vulkan non-dispatchable handle. It transfers no
95/// ownership and grants no memory access. Zero represents `VK_NULL_HANDLE`.
96#[derive(Clone, Copy, PartialEq, Eq, Hash)]
97pub struct VulkanHandle {
98    bits: u64,
99    _thread_affine: PhantomData<Rc<()>>,
100}
101
102impl VulkanHandle {
103    /// Null Vulkan non-dispatchable handle.
104    pub const NULL: Self = Self {
105        bits: 0,
106        _thread_affine: PhantomData,
107    };
108
109    /// Creates a borrowed Vulkan handle from its native bit pattern.
110    ///
111    /// # Safety
112    ///
113    /// The bit pattern must have the Vulkan handle type required by every API
114    /// that receives it. The host must keep that object valid and synchronized
115    /// for the documented borrow window.
116    pub unsafe fn from_bits(bits: u64) -> Self {
117        Self {
118            bits,
119            _thread_affine: PhantomData,
120        }
121    }
122
123    /// Returns this Vulkan handle's native bit pattern.
124    pub fn bits(self) -> u64 {
125        self.bits
126    }
127
128    /// Returns whether this value represents `VK_NULL_HANDLE`.
129    pub fn is_null(self) -> bool {
130        self.bits == 0
131    }
132}
133
134impl fmt::Debug for VulkanHandle {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "VulkanHandle(0x{:x})", self.bits)
137    }
138}
139
140/// Borrowed Vulkan non-dispatchable handle tied to an active texture frame.
141#[derive(Clone, Copy, PartialEq, Eq, Hash)]
142pub struct FrameVulkanHandle<'frame> {
143    bits: u64,
144    _frame: PhantomData<&'frame ()>,
145    _thread_affine: PhantomData<Rc<()>>,
146}
147
148impl<'frame> FrameVulkanHandle<'frame> {
149    fn new(bits: u64) -> Self {
150        Self {
151            bits,
152            _frame: PhantomData,
153            _thread_affine: PhantomData,
154        }
155    }
156
157    /// Returns this Vulkan handle's native bit pattern.
158    ///
159    /// # Safety
160    ///
161    /// The returned integer no longer carries this value's frame lifetime. Use
162    /// it only while the borrowed frame remains open and satisfy the Vulkan
163    /// synchronization requirements for the image or image view.
164    pub unsafe fn bits(self) -> u64 {
165        self.bits
166    }
167
168    /// Returns whether this value represents `VK_NULL_HANDLE`.
169    pub fn is_null(self) -> bool {
170        self.bits == 0
171    }
172}
173
174impl fmt::Debug for FrameVulkanHandle<'_> {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(f, "FrameVulkanHandle(0x{:x})", self.bits)
177    }
178}
179
180/// Borrowed opaque native address whose validity is tied to an active texture
181/// frame. It does not own, retain, dereference, or validate the pointed-to
182/// object.
183#[derive(Clone, Copy, PartialEq, Eq, Hash)]
184pub struct FrameNativePointer<'frame> {
185    address: usize,
186    _frame: PhantomData<&'frame ()>,
187    _thread_affine: PhantomData<Rc<()>>,
188}
189
190impl<'frame> FrameNativePointer<'frame> {
191    unsafe fn from_ptr<T>(ptr: *mut T) -> Self {
192        Self {
193            address: ptr as usize,
194            _frame: PhantomData,
195            _thread_affine: PhantomData,
196        }
197    }
198
199    /// Returns this opaque value as an integer address.
200    ///
201    /// # Safety
202    ///
203    /// The returned integer no longer carries this value's frame lifetime. The
204    /// caller must use it only while the borrowed frame remains open and must
205    /// satisfy the backend API's type, synchronization, and thread-affinity
206    /// requirements.
207    pub unsafe fn address(self) -> usize {
208        self.address
209    }
210
211    /// Returns whether this value is null.
212    pub fn is_null(self) -> bool {
213        self.address == 0
214    }
215
216    /// Reconstructs a raw pointer for a backend interop call.
217    ///
218    /// # Safety
219    ///
220    /// The caller must choose the correct pointer type and uphold the lifetime,
221    /// thread-affinity, synchronization, and aliasing requirements of the
222    /// backend API that will receive the pointer.
223    pub unsafe fn as_ptr<T>(self) -> *mut T {
224        self.address as *mut T
225    }
226}
227
228impl fmt::Debug for FrameNativePointer<'_> {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "FrameNativePointer(0x{:x})", self.address)
231    }
232}
233
234/// Borrowed OpenGL texture object name tied to an active texture frame.
235#[derive(Clone, Copy, PartialEq, Eq, Hash)]
236pub struct FrameOpenGLTextureName<'frame> {
237    name: u32,
238    _frame: PhantomData<&'frame ()>,
239    _thread_affine: PhantomData<Rc<()>>,
240}
241
242impl<'frame> FrameOpenGLTextureName<'frame> {
243    fn new(name: u32) -> Self {
244        Self {
245            name,
246            _frame: PhantomData,
247            _thread_affine: PhantomData,
248        }
249    }
250
251    /// Returns whether this OpenGL texture object name is zero.
252    pub fn is_zero(self) -> bool {
253        self.name == 0
254    }
255
256    /// Returns the OpenGL texture object name.
257    ///
258    /// # Safety
259    ///
260    /// The returned integer no longer carries this value's frame lifetime. Use
261    /// it only while the borrowed frame remains open and satisfy OpenGL
262    /// synchronization and context-share-group requirements.
263    pub unsafe fn value(self) -> u32 {
264        self.name
265    }
266}
267
268impl fmt::Debug for FrameOpenGLTextureName<'_> {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        write!(f, "FrameOpenGLTextureName({})", self.name)
271    }
272}
273
274mod query;
275pub use query::{
276    FeatureStateSelector, QueriedFeature, RenderedFeatureQueryOptions, RenderedQueryGeometry,
277    SourceFeatureQueryOptions,
278};
279#[derive(Debug, Clone, PartialEq)]
280#[non_exhaustive]
281pub struct RenderTargetExtent {
282    pub width: u32,
283    pub height: u32,
284    pub scale_factor: f64,
285}
286
287impl RenderTargetExtent {
288    pub fn new(width: u32, height: u32, scale_factor: f64) -> Self {
289        Self {
290            width,
291            height,
292            scale_factor,
293        }
294    }
295
296    pub(crate) fn to_core(&self) -> maplibre_core::render::RenderTargetExtentFields {
297        maplibre_core::render::RenderTargetExtentFields {
298            width: self.width,
299            height: self.height,
300            scale_factor: self.scale_factor,
301        }
302    }
303
304    /// Returns this extent's physical device-pixel size as
305    /// `ceil(logical * scale_factor)` per dimension. Surface and session-owned
306    /// texture targets are sized this way; borrowed texture targets state their
307    /// physical size instead.
308    pub fn physical_size(&self) -> Result<(u32, u32)> {
309        let native = maplibre_core::render::render_target_extent_to_native(self.to_core());
310        let mut width = 0u32;
311        let mut height = 0u32;
312        // SAFETY: native is a fully initialized extent and both out pointers
313        // reference live locals for the duration of the call.
314        maplibre_core::check(unsafe {
315            sys::mln_render_target_extent_physical_size(&native, &mut width, &mut height)
316        })?;
317        Ok((width, height))
318    }
319}
320
321impl Default for RenderTargetExtent {
322    fn default() -> Self {
323        Self::new(256, 256, 1.0)
324    }
325}
326
327#[derive(Debug, Clone, PartialEq)]
328#[non_exhaustive]
329pub struct MetalContextDescriptor {
330    pub device: NativePointer,
331}
332
333impl MetalContextDescriptor {
334    pub fn new(device: NativePointer) -> Self {
335        Self { device }
336    }
337
338    pub(crate) fn to_core(&self) -> maplibre_core::render::MetalContextDescriptorFields {
339        maplibre_core::render::MetalContextDescriptorFields {
340            device: self.device.as_void_ptr(),
341        }
342    }
343}
344
345impl Default for MetalContextDescriptor {
346    fn default() -> Self {
347        Self::new(NativePointer::NULL)
348    }
349}
350
351#[derive(Debug, Clone, PartialEq)]
352#[non_exhaustive]
353pub struct VulkanContextDescriptor {
354    pub instance: NativePointer,
355    pub physical_device: NativePointer,
356    pub device: NativePointer,
357    pub graphics_queue: NativePointer,
358    pub graphics_queue_family_index: u32,
359    pub get_instance_proc_addr: NativePointer,
360    pub get_device_proc_addr: NativePointer,
361}
362
363impl VulkanContextDescriptor {
364    #[allow(clippy::too_many_arguments)]
365    pub fn new(
366        instance: NativePointer,
367        physical_device: NativePointer,
368        device: NativePointer,
369        graphics_queue: NativePointer,
370        graphics_queue_family_index: u32,
371    ) -> Self {
372        Self {
373            instance,
374            physical_device,
375            device,
376            graphics_queue,
377            graphics_queue_family_index,
378            get_instance_proc_addr: NativePointer::NULL,
379            get_device_proc_addr: NativePointer::NULL,
380        }
381    }
382
383    pub(crate) fn to_core(&self) -> maplibre_core::render::VulkanContextDescriptorFields {
384        maplibre_core::render::VulkanContextDescriptorFields {
385            instance: self.instance.as_void_ptr(),
386            physical_device: self.physical_device.as_void_ptr(),
387            device: self.device.as_void_ptr(),
388            graphics_queue: self.graphics_queue.as_void_ptr(),
389            graphics_queue_family_index: self.graphics_queue_family_index,
390            get_instance_proc_addr: self.get_instance_proc_addr.as_void_ptr(),
391            get_device_proc_addr: self.get_device_proc_addr.as_void_ptr(),
392        }
393    }
394}
395
396impl Default for VulkanContextDescriptor {
397    fn default() -> Self {
398        Self::new(
399            NativePointer::NULL,
400            NativePointer::NULL,
401            NativePointer::NULL,
402            NativePointer::NULL,
403            0,
404        )
405    }
406}
407
408/// Browser WebGPU device a session renders with.
409///
410/// A browser host owns its WebGPU objects, so a session borrows these rather
411/// than creating any of them. They must stay valid until the session is
412/// detached or closed.
413#[derive(Debug, Clone, PartialEq)]
414#[non_exhaustive]
415pub struct WebGpuContextDescriptor {
416    /// Optional for texture sessions.
417    pub instance: NativePointer,
418    pub device: NativePointer,
419    /// Optional; null uses the device's default queue. A non-null queue must
420    /// belong to `device`.
421    pub queue: NativePointer,
422}
423
424impl WebGpuContextDescriptor {
425    pub fn new(device: NativePointer) -> Self {
426        Self {
427            instance: NativePointer::NULL,
428            device,
429            queue: NativePointer::NULL,
430        }
431    }
432
433    pub(crate) fn to_core(&self) -> maplibre_core::render::WebGpuContextDescriptorFields {
434        maplibre_core::render::WebGpuContextDescriptorFields {
435            instance: self.instance.as_void_ptr(),
436            device: self.device.as_void_ptr(),
437            queue: self.queue.as_void_ptr(),
438        }
439    }
440}
441
442impl Default for WebGpuContextDescriptor {
443    fn default() -> Self {
444        Self::new(NativePointer::NULL)
445    }
446}
447
448#[derive(Debug, Clone, PartialEq)]
449#[non_exhaustive]
450pub struct WglContextDescriptor {
451    pub device_context: NativePointer,
452    /// Borrowed `HGLRC` whose share group the session context joins. Required
453    /// under shared ownership. A dedicated session joins no share group, so it
454    /// is null there.
455    pub share_context: NativePointer,
456    pub get_proc_address: NativePointer,
457    /// Whether the session shares its thread with host graphics work.
458    pub ownership: OpenGLContextOwnership,
459}
460
461impl WglContextDescriptor {
462    pub fn new(device_context: NativePointer, share_context: NativePointer) -> Self {
463        Self {
464            device_context,
465            share_context,
466            get_proc_address: NativePointer::NULL,
467            ownership: OpenGLContextOwnership::Shared,
468        }
469    }
470
471    pub(crate) fn to_core(&self) -> maplibre_core::render::WglContextDescriptorFields {
472        maplibre_core::render::WglContextDescriptorFields {
473            device_context: self.device_context.as_void_ptr(),
474            share_context: self.share_context.as_void_ptr(),
475            get_proc_address: self.get_proc_address.as_void_ptr(),
476            ownership: self.ownership.as_raw(),
477        }
478    }
479}
480
481impl Default for WglContextDescriptor {
482    fn default() -> Self {
483        Self::new(NativePointer::NULL, NativePointer::NULL)
484    }
485}
486
487#[derive(Debug, Clone, PartialEq)]
488#[non_exhaustive]
489pub struct EglContextDescriptor {
490    pub display: NativePointer,
491    pub config: NativePointer,
492    /// Borrowed `EGLContext` whose share group the session context joins.
493    /// Required under shared ownership, where the session also takes its client
494    /// API from this context. A dedicated session joins no share group, so it
495    /// is null there and names [`client_api`](Self::client_api) instead.
496    pub share_context: NativePointer,
497    /// Client API the session creates its context for. Required under dedicated
498    /// ownership. A shared session queries
499    /// [`share_context`](Self::share_context) for it, so this is ignored there.
500    pub client_api: OpenGLClientApi,
501    pub get_proc_address: NativePointer,
502    /// Whether the session shares its thread with host graphics work.
503    pub ownership: OpenGLContextOwnership,
504}
505
506impl EglContextDescriptor {
507    pub fn new(
508        display: NativePointer,
509        config: NativePointer,
510        share_context: NativePointer,
511    ) -> Self {
512        Self {
513            display,
514            config,
515            share_context,
516            client_api: OpenGLClientApi::Unspecified,
517            get_proc_address: NativePointer::NULL,
518            ownership: OpenGLContextOwnership::Shared,
519        }
520    }
521
522    pub(crate) fn to_core(&self) -> maplibre_core::render::EglContextDescriptorFields {
523        maplibre_core::render::EglContextDescriptorFields {
524            display: self.display.as_void_ptr(),
525            config: self.config.as_void_ptr(),
526            share_context: self.share_context.as_void_ptr(),
527            client_api: self.client_api.as_raw(),
528            get_proc_address: self.get_proc_address.as_void_ptr(),
529            ownership: self.ownership.as_raw(),
530        }
531    }
532}
533
534impl Default for EglContextDescriptor {
535    fn default() -> Self {
536        Self::new(
537            NativePointer::NULL,
538            NativePointer::NULL,
539            NativePointer::NULL,
540        )
541    }
542}
543
544/// Browser WebGL context a session renders into.
545///
546/// The host creates the context and keeps owning it; a session shares it rather
547/// than holding it exclusively. `context` is an
548/// `EMSCRIPTEN_WEBGL_CONTEXT_HANDLE`, which the native library requires to be
549/// positive.
550#[derive(Debug, Clone, PartialEq)]
551#[non_exhaustive]
552pub struct WebGlContextDescriptor {
553    pub context: i32,
554}
555
556impl WebGlContextDescriptor {
557    pub fn new(context: i32) -> Self {
558        Self { context }
559    }
560
561    pub(crate) fn to_core(&self) -> maplibre_core::render::WebGlContextDescriptorFields {
562        maplibre_core::render::WebGlContextDescriptorFields {
563            context: self.context,
564        }
565    }
566}
567
568/// OpenGL platform context a render session draws through.
569///
570/// Each platform descriptor carries its own thread ownership. A browser session
571/// renders through the host's own WebGL context, so it is shared only.
572#[derive(Debug, Clone, PartialEq)]
573#[non_exhaustive]
574pub enum OpenGLContextDescriptor {
575    Wgl(WglContextDescriptor),
576    Egl(EglContextDescriptor),
577    WebGl(WebGlContextDescriptor),
578}
579
580impl OpenGLContextDescriptor {
581    pub(crate) fn to_core(&self) -> maplibre_core::render::OpenGLContextDescriptorFields {
582        match self {
583            Self::Wgl(descriptor) => {
584                maplibre_core::render::OpenGLContextDescriptorFields::Wgl(descriptor.to_core())
585            }
586            Self::Egl(descriptor) => {
587                maplibre_core::render::OpenGLContextDescriptorFields::Egl(descriptor.to_core())
588            }
589            Self::WebGl(descriptor) => {
590                maplibre_core::render::OpenGLContextDescriptorFields::WebGl(descriptor.to_core())
591            }
592        }
593    }
594}
595
596#[derive(Debug, Clone, PartialEq)]
597#[non_exhaustive]
598pub struct MetalSurfaceDescriptor {
599    pub extent: RenderTargetExtent,
600    pub context: MetalContextDescriptor,
601    pub layer: NativePointer,
602}
603
604impl MetalSurfaceDescriptor {
605    pub fn new(
606        extent: RenderTargetExtent,
607        context: MetalContextDescriptor,
608        layer: NativePointer,
609    ) -> Self {
610        Self {
611            extent,
612            context,
613            layer,
614        }
615    }
616
617    pub(crate) fn to_native(&self) -> sys::mln_metal_surface_descriptor {
618        maplibre_core::render::metal_surface_descriptor_to_native(
619            maplibre_core::render::MetalSurfaceDescriptorFields {
620                extent: self.extent.to_core(),
621                context: self.context.to_core(),
622                layer: self.layer.as_void_ptr(),
623            },
624        )
625    }
626}
627
628#[derive(Debug, Clone, PartialEq)]
629#[non_exhaustive]
630pub struct VulkanSurfaceDescriptor {
631    pub extent: RenderTargetExtent,
632    pub context: VulkanContextDescriptor,
633    pub surface: VulkanHandle,
634}
635
636impl VulkanSurfaceDescriptor {
637    pub fn new(
638        extent: RenderTargetExtent,
639        context: VulkanContextDescriptor,
640        surface: VulkanHandle,
641    ) -> Self {
642        Self {
643            extent,
644            context,
645            surface,
646        }
647    }
648
649    pub(crate) fn to_native(&self) -> sys::mln_vulkan_surface_descriptor {
650        maplibre_core::render::vulkan_surface_descriptor_to_native(
651            maplibre_core::render::VulkanSurfaceDescriptorFields {
652                extent: self.extent.to_core(),
653                context: self.context.to_core(),
654                surface: self.surface.bits(),
655            },
656        )
657    }
658}
659
660/// WebGPU native surface session attachment options.
661///
662/// The surface is borrowed: the host creates it from whatever it presents to,
663/// which in a browser is a canvas, and keeps it alive for the session. The
664/// format is the host's too, because a surface reports what it supports through
665/// its adapter, which this descriptor does not carry.
666#[derive(Debug, Clone, PartialEq)]
667#[non_exhaustive]
668pub struct WebGpuSurfaceDescriptor {
669    pub extent: RenderTargetExtent,
670    pub context: WebGpuContextDescriptor,
671    pub surface: NativePointer,
672    pub format: u32,
673}
674
675impl WebGpuSurfaceDescriptor {
676    pub fn new(
677        extent: RenderTargetExtent,
678        context: WebGpuContextDescriptor,
679        surface: NativePointer,
680        format: u32,
681    ) -> Self {
682        Self {
683            extent,
684            context,
685            surface,
686            format,
687        }
688    }
689
690    pub(crate) fn to_native(&self) -> sys::mln_webgpu_surface_descriptor {
691        maplibre_core::render::webgpu_surface_descriptor_to_native(
692            maplibre_core::render::WebGpuSurfaceDescriptorFields {
693                extent: self.extent.to_core(),
694                context: self.context.to_core(),
695                surface: self.surface.as_void_ptr(),
696                format: self.format,
697            },
698        )
699    }
700}
701
702#[derive(Debug, Clone, PartialEq)]
703#[non_exhaustive]
704pub struct OpenGLSurfaceDescriptor {
705    pub extent: RenderTargetExtent,
706    pub context: OpenGLContextDescriptor,
707    pub surface: NativePointer,
708}
709
710impl OpenGLSurfaceDescriptor {
711    pub fn new(
712        extent: RenderTargetExtent,
713        context: OpenGLContextDescriptor,
714        surface: NativePointer,
715    ) -> Self {
716        Self {
717            extent,
718            context,
719            surface,
720        }
721    }
722
723    pub(crate) fn to_native(&self) -> sys::mln_opengl_surface_descriptor {
724        maplibre_core::render::opengl_surface_descriptor_to_native(
725            maplibre_core::render::OpenGLSurfaceDescriptorFields {
726                extent: self.extent.to_core(),
727                context: self.context.to_core(),
728                surface: self.surface.as_void_ptr(),
729            },
730        )
731    }
732}
733
734#[derive(Debug, Clone, PartialEq)]
735#[non_exhaustive]
736pub struct MetalOwnedTextureDescriptor {
737    pub extent: RenderTargetExtent,
738    pub context: MetalContextDescriptor,
739}
740
741impl MetalOwnedTextureDescriptor {
742    pub fn new(extent: RenderTargetExtent, context: MetalContextDescriptor) -> Self {
743        Self { extent, context }
744    }
745
746    pub(crate) fn to_native(&self) -> sys::mln_metal_owned_texture_descriptor {
747        maplibre_core::render::metal_owned_texture_descriptor_to_native(
748            maplibre_core::render::MetalOwnedTextureDescriptorFields {
749                extent: self.extent.to_core(),
750                context: self.context.to_core(),
751            },
752        )
753    }
754}
755
756#[derive(Debug, Clone, PartialEq)]
757#[non_exhaustive]
758pub struct MetalBorrowedTextureDescriptor {
759    pub extent: RenderTargetExtent,
760    /// Physical texture size in device pixels. The texture is sized by its
761    /// owner, so this is stated rather than derived from `extent`.
762    pub physical_width: u32,
763    pub physical_height: u32,
764    pub texture: NativePointer,
765}
766
767impl MetalBorrowedTextureDescriptor {
768    pub fn new(
769        extent: RenderTargetExtent,
770        physical_width: u32,
771        physical_height: u32,
772        texture: NativePointer,
773    ) -> Self {
774        Self {
775            extent,
776            physical_width,
777            physical_height,
778            texture,
779        }
780    }
781
782    pub(crate) fn to_native(&self) -> sys::mln_metal_borrowed_texture_descriptor {
783        maplibre_core::render::metal_borrowed_texture_descriptor_to_native(
784            maplibre_core::render::MetalBorrowedTextureDescriptorFields {
785                extent: self.extent.to_core(),
786                physical_width: self.physical_width,
787                physical_height: self.physical_height,
788                texture: self.texture.as_void_ptr(),
789            },
790        )
791    }
792}
793
794#[derive(Debug, Clone, PartialEq)]
795#[non_exhaustive]
796pub struct VulkanOwnedTextureDescriptor {
797    pub extent: RenderTargetExtent,
798    pub context: VulkanContextDescriptor,
799}
800
801impl VulkanOwnedTextureDescriptor {
802    pub fn new(extent: RenderTargetExtent, context: VulkanContextDescriptor) -> Self {
803        Self { extent, context }
804    }
805
806    pub(crate) fn to_native(&self) -> sys::mln_vulkan_owned_texture_descriptor {
807        maplibre_core::render::vulkan_owned_texture_descriptor_to_native(
808            maplibre_core::render::VulkanOwnedTextureDescriptorFields {
809                extent: self.extent.to_core(),
810                context: self.context.to_core(),
811            },
812        )
813    }
814}
815
816#[derive(Debug, Clone, PartialEq)]
817#[non_exhaustive]
818pub struct VulkanBorrowedTextureDescriptor {
819    pub extent: RenderTargetExtent,
820    /// Physical image size in device pixels. The image is sized by its owner,
821    /// so this is stated rather than derived from `extent`.
822    pub physical_width: u32,
823    pub physical_height: u32,
824    pub context: VulkanContextDescriptor,
825    pub image: VulkanHandle,
826    pub image_view: VulkanHandle,
827    pub format: u32,
828    pub initial_layout: u32,
829    pub final_layout: u32,
830}
831
832impl VulkanBorrowedTextureDescriptor {
833    #[allow(clippy::too_many_arguments)]
834    pub fn new(
835        extent: RenderTargetExtent,
836        physical_width: u32,
837        physical_height: u32,
838        context: VulkanContextDescriptor,
839        image: VulkanHandle,
840        image_view: VulkanHandle,
841        format: u32,
842        initial_layout: u32,
843        final_layout: u32,
844    ) -> Self {
845        Self {
846            extent,
847            physical_width,
848            physical_height,
849            context,
850            image,
851            image_view,
852            format,
853            initial_layout,
854            final_layout,
855        }
856    }
857
858    pub(crate) fn to_native(&self) -> sys::mln_vulkan_borrowed_texture_descriptor {
859        maplibre_core::render::vulkan_borrowed_texture_descriptor_to_native(
860            maplibre_core::render::VulkanBorrowedTextureDescriptorFields {
861                extent: self.extent.to_core(),
862                physical_width: self.physical_width,
863                physical_height: self.physical_height,
864                context: self.context.to_core(),
865                image: self.image.bits(),
866                image_view: self.image_view.bits(),
867                format: self.format,
868                initial_layout: self.initial_layout,
869                final_layout: self.final_layout,
870            },
871        )
872    }
873}
874
875#[derive(Debug, Clone, PartialEq)]
876#[non_exhaustive]
877pub struct WebGpuOwnedTextureDescriptor {
878    pub extent: RenderTargetExtent,
879    pub context: WebGpuContextDescriptor,
880}
881
882impl WebGpuOwnedTextureDescriptor {
883    pub fn new(extent: RenderTargetExtent, context: WebGpuContextDescriptor) -> Self {
884        Self { extent, context }
885    }
886
887    pub(crate) fn to_native(&self) -> sys::mln_webgpu_owned_texture_descriptor {
888        maplibre_core::render::webgpu_owned_texture_descriptor_to_native(
889            maplibre_core::render::WebGpuOwnedTextureDescriptorFields {
890                extent: self.extent.to_core(),
891                context: self.context.to_core(),
892            },
893        )
894    }
895}
896
897#[derive(Debug, Clone, PartialEq)]
898#[non_exhaustive]
899pub struct WebGpuBorrowedTextureDescriptor {
900    pub extent: RenderTargetExtent,
901    /// Physical texture size in device pixels. The texture is sized by its
902    /// owner, so this is stated rather than derived from `extent`.
903    pub physical_width: u32,
904    pub physical_height: u32,
905    pub context: WebGpuContextDescriptor,
906    pub texture: NativePointer,
907    pub texture_view: NativePointer,
908    /// Backend-native `WGPUTextureFormat` value.
909    pub format: u32,
910}
911
912impl WebGpuBorrowedTextureDescriptor {
913    #[allow(clippy::too_many_arguments)]
914    pub fn new(
915        extent: RenderTargetExtent,
916        physical_width: u32,
917        physical_height: u32,
918        context: WebGpuContextDescriptor,
919        texture: NativePointer,
920        texture_view: NativePointer,
921        format: u32,
922    ) -> Self {
923        Self {
924            extent,
925            physical_width,
926            physical_height,
927            context,
928            texture,
929            texture_view,
930            format,
931        }
932    }
933
934    pub(crate) fn to_native(&self) -> sys::mln_webgpu_borrowed_texture_descriptor {
935        maplibre_core::render::webgpu_borrowed_texture_descriptor_to_native(
936            maplibre_core::render::WebGpuBorrowedTextureDescriptorFields {
937                extent: self.extent.to_core(),
938                physical_width: self.physical_width,
939                physical_height: self.physical_height,
940                context: self.context.to_core(),
941                texture: self.texture.as_void_ptr(),
942                texture_view: self.texture_view.as_void_ptr(),
943                format: self.format,
944            },
945        )
946    }
947}
948
949#[derive(Debug, Clone, PartialEq)]
950#[non_exhaustive]
951pub struct OpenGLOwnedTextureDescriptor {
952    pub extent: RenderTargetExtent,
953    pub context: OpenGLContextDescriptor,
954}
955
956impl OpenGLOwnedTextureDescriptor {
957    pub fn new(extent: RenderTargetExtent, context: OpenGLContextDescriptor) -> Self {
958        Self { extent, context }
959    }
960
961    pub(crate) fn to_native(&self) -> sys::mln_opengl_owned_texture_descriptor {
962        maplibre_core::render::opengl_owned_texture_descriptor_to_native(
963            maplibre_core::render::OpenGLOwnedTextureDescriptorFields {
964                extent: self.extent.to_core(),
965                context: self.context.to_core(),
966            },
967        )
968    }
969}
970
971#[derive(Debug, Clone, PartialEq)]
972#[non_exhaustive]
973pub struct OpenGLBorrowedTextureDescriptor {
974    pub extent: RenderTargetExtent,
975    /// Physical texture size in device pixels. The texture is sized by its
976    /// owner, so this is stated rather than derived from `extent`.
977    pub physical_width: u32,
978    pub physical_height: u32,
979    pub context: OpenGLContextDescriptor,
980    pub texture: u32,
981    pub target: u32,
982}
983
984impl OpenGLBorrowedTextureDescriptor {
985    pub fn new(
986        extent: RenderTargetExtent,
987        physical_width: u32,
988        physical_height: u32,
989        context: OpenGLContextDescriptor,
990        texture: u32,
991        target: u32,
992    ) -> Self {
993        Self {
994            extent,
995            physical_width,
996            physical_height,
997            context,
998            texture,
999            target,
1000        }
1001    }
1002
1003    pub(crate) fn to_native(&self) -> sys::mln_opengl_borrowed_texture_descriptor {
1004        maplibre_core::render::opengl_borrowed_texture_descriptor_to_native(
1005            maplibre_core::render::OpenGLBorrowedTextureDescriptorFields {
1006                extent: self.extent.to_core(),
1007                physical_width: self.physical_width,
1008                physical_height: self.physical_height,
1009                context: self.context.to_core(),
1010                texture: self.texture,
1011                target: self.target,
1012            },
1013        )
1014    }
1015}
1016
1017#[derive(Debug)]
1018struct RenderSessionState {
1019    handle: ThreadAffineNativeHandle<sys::mln_render_session>,
1020    detached: Cell<bool>,
1021    frame_acquired: Cell<bool>,
1022}
1023
1024impl RenderSessionState {
1025    fn new(native: sys::mln_render_session) -> Result<Self> {
1026        // SAFETY: native came from a successful render-session attach call and is
1027        // paired with the matching render-session destroy function.
1028        let handle = unsafe {
1029            ThreadAffineNativeHandle::from_handle(
1030                native,
1031                sys::mln_render_session_destroy,
1032                "mln_render_session",
1033            )
1034        }?;
1035        Ok(Self {
1036            handle,
1037            detached: Cell::new(false),
1038            frame_acquired: Cell::new(false),
1039        })
1040    }
1041
1042    fn ensure_no_frame_acquired(&self) -> Result<()> {
1043        if self.frame_acquired.get() {
1044            Err(frame_acquired_error())
1045        } else {
1046            Ok(())
1047        }
1048    }
1049
1050    fn native(&self) -> Result<sys::mln_render_session> {
1051        self.handle
1052            .live_handle()
1053            .ok_or_else(|| closed_handle_error("RenderSessionHandle"))
1054    }
1055
1056    fn close(&self) -> Result<()> {
1057        self.handle.close()
1058    }
1059}
1060
1061/// Outcome of a successful [`RenderSessionHandle::render_update`] call.
1062#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1063#[non_exhaustive]
1064pub struct RenderUpdate {
1065    /// Which outcome the call reached; see [`RenderResult`] for the wake each
1066    /// variant names.
1067    pub result: RenderResult,
1068    /// Whether the map asked for another frame while it rendered this one, as
1069    /// during an ongoing camera transition. Set only when `result` is
1070    /// [`RenderResult::Rendered`]; false for every other outcome. This is the
1071    /// same signal the render-frame-finished event carries, delivered here
1072    /// without the event round trip, so a host can re-arm its frame loop
1073    /// before it drains events.
1074    pub needs_repaint: bool,
1075}
1076
1077/// Render session handle bound to the thread that attached it.
1078///
1079/// The session holds no Rust-level retention of its map. Native keeps the map
1080/// alive instead: destroying a map fails while a session is attached to it.
1081pub struct RenderSessionHandle {
1082    inner: Rc<RenderSessionState>,
1083}
1084
1085impl fmt::Debug for RenderSessionHandle {
1086    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1087        f.debug_struct("RenderSessionHandle")
1088            .field("closed", &self.inner.handle.is_closed())
1089            .field("detached", &self.inner.detached.get())
1090            .finish()
1091    }
1092}
1093
1094/// Render session after backend resources have been detached.
1095///
1096/// A detached session holds no reference to its former map, so it stays
1097/// destroyable after that map closes.
1098pub struct DetachedRenderSessionHandle {
1099    inner: Rc<RenderSessionState>,
1100}
1101
1102impl fmt::Debug for DetachedRenderSessionHandle {
1103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1104        f.debug_struct("DetachedRenderSessionHandle")
1105            .field("closed", &self.inner.handle.is_closed())
1106            .finish()
1107    }
1108}
1109
1110impl DetachedRenderSessionHandle {
1111    /// Explicitly destroys the detached render session.
1112    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1113        if let Err(error) = self.inner.close() {
1114            return Err(HandleOperationError::new(error, self));
1115        }
1116        Ok(())
1117    }
1118}
1119
1120fn frame_acquired_error() -> crate::Error {
1121    crate::Error::new(
1122        crate::ErrorKind::InvalidState,
1123        None,
1124        "render session has an acquired texture frame",
1125    )
1126}
1127
1128/// Copied metadata for an acquired Metal session-owned texture frame.
1129///
1130/// Backend pointers are exposed by [`MetalOwnedTextureFrameHandle`] so their
1131/// lifetime stays tied to the open frame handle.
1132#[derive(Debug, Clone, Copy, PartialEq)]
1133#[non_exhaustive]
1134pub struct MetalOwnedTextureFrame {
1135    pub generation: u64,
1136    pub width: u32,
1137    pub height: u32,
1138    pub scale_factor: f64,
1139    pub frame_id: u64,
1140    pub pixel_format: u64,
1141}
1142
1143impl MetalOwnedTextureFrame {
1144    fn from_native(raw: &sys::mln_metal_owned_texture_frame) -> Self {
1145        Self {
1146            generation: raw.generation,
1147            width: raw.width,
1148            height: raw.height,
1149            scale_factor: raw.scale_factor,
1150            frame_id: raw.frame_id,
1151            pixel_format: raw.pixel_format,
1152        }
1153    }
1154}
1155
1156/// Copied metadata for an acquired Vulkan session-owned texture frame.
1157///
1158/// Backend handles are exposed by [`VulkanOwnedTextureFrameHandle`] so their
1159/// lifetime stays tied to the open frame handle.
1160#[derive(Debug, Clone, Copy, PartialEq)]
1161#[non_exhaustive]
1162pub struct VulkanOwnedTextureFrame {
1163    pub generation: u64,
1164    pub width: u32,
1165    pub height: u32,
1166    pub scale_factor: f64,
1167    pub frame_id: u64,
1168    pub format: u32,
1169    pub layout: u32,
1170}
1171
1172impl VulkanOwnedTextureFrame {
1173    fn from_native(raw: &sys::mln_vulkan_owned_texture_frame) -> Self {
1174        Self {
1175            generation: raw.generation,
1176            width: raw.width,
1177            height: raw.height,
1178            scale_factor: raw.scale_factor,
1179            frame_id: raw.frame_id,
1180            format: raw.format,
1181            layout: raw.layout,
1182        }
1183    }
1184}
1185
1186/// Copied metadata for an acquired WebGPU session-owned texture frame.
1187///
1188/// Backend pointers are exposed by [`WebGpuOwnedTextureFrameHandle`] so their
1189/// lifetime stays tied to the open frame handle.
1190#[derive(Debug, Clone, Copy, PartialEq)]
1191#[non_exhaustive]
1192pub struct WebGpuOwnedTextureFrame {
1193    pub generation: u64,
1194    pub width: u32,
1195    pub height: u32,
1196    pub scale_factor: f64,
1197    pub frame_id: u64,
1198    pub format: u32,
1199}
1200
1201impl WebGpuOwnedTextureFrame {
1202    fn from_native(raw: &sys::mln_webgpu_owned_texture_frame) -> Self {
1203        Self {
1204            generation: raw.generation,
1205            width: raw.width,
1206            height: raw.height,
1207            scale_factor: raw.scale_factor,
1208            frame_id: raw.frame_id,
1209            format: raw.format,
1210        }
1211    }
1212}
1213
1214/// Copied metadata for an acquired OpenGL session-owned texture frame.
1215///
1216/// The texture object name is exposed by [`OpenGLOwnedTextureFrameHandle`] so
1217/// its lifetime stays tied to the open frame handle.
1218#[derive(Debug, Clone, Copy, PartialEq)]
1219#[non_exhaustive]
1220pub struct OpenGLOwnedTextureFrame {
1221    pub generation: u64,
1222    pub width: u32,
1223    pub height: u32,
1224    pub scale_factor: f64,
1225    pub frame_id: u64,
1226    pub target: u32,
1227    pub internal_format: u32,
1228    pub format: u32,
1229    pub type_: u32,
1230}
1231
1232impl OpenGLOwnedTextureFrame {
1233    fn from_native(raw: &sys::mln_opengl_owned_texture_frame) -> Self {
1234        Self {
1235            generation: raw.generation,
1236            width: raw.width,
1237            height: raw.height,
1238            scale_factor: raw.scale_factor,
1239            frame_id: raw.frame_id,
1240            target: raw.target,
1241            internal_format: raw.internal_format,
1242            format: raw.format,
1243            type_: raw.type_,
1244        }
1245    }
1246}
1247
1248/// RAII guard for an acquired Metal session-owned texture frame.
1249///
1250/// Releasing the guard ends the borrow of the backend Metal texture and device.
1251pub struct MetalOwnedTextureFrameHandle {
1252    session: Rc<RenderSessionState>,
1253    raw: sys::mln_metal_owned_texture_frame,
1254    frame: MetalOwnedTextureFrame,
1255    closed: Cell<bool>,
1256    _thread_affine: PhantomData<Rc<()>>,
1257}
1258
1259impl fmt::Debug for MetalOwnedTextureFrameHandle {
1260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1261        f.debug_struct("MetalOwnedTextureFrameHandle")
1262            .field("closed", &self.closed.get())
1263            .field("frame", &self.frame)
1264            .finish()
1265    }
1266}
1267
1268impl MetalOwnedTextureFrameHandle {
1269    /// Returns copied metadata for this acquired frame.
1270    pub fn frame(&self) -> Result<&MetalOwnedTextureFrame> {
1271        if self.closed.get() {
1272            Err(closed_handle_error("MetalOwnedTextureFrameHandle"))
1273        } else {
1274            Ok(&self.frame)
1275        }
1276    }
1277    /// Returns the borrowed Metal texture pointer for backend interop.
1278    ///
1279    /// # Safety
1280    ///
1281    /// The returned pointer is valid only while this frame handle remains open.
1282    /// The caller must not store or use it after frame release and must satisfy
1283    /// Metal synchronization and thread-affinity requirements.
1284    pub unsafe fn texture(&self) -> Result<FrameNativePointer<'_>> {
1285        if self.closed.get() {
1286            Err(closed_handle_error("MetalOwnedTextureFrameHandle"))
1287        } else {
1288            // SAFETY: The active native frame owns the validity contract for
1289            // this borrowed backend handle until release.
1290            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture) })
1291        }
1292    }
1293
1294    /// Returns the borrowed Metal device pointer for backend interop.
1295    ///
1296    /// # Safety
1297    ///
1298    /// The returned pointer has the same lifetime and synchronization
1299    /// requirements as [`MetalOwnedTextureFrameHandle::texture`].
1300    pub unsafe fn device(&self) -> Result<FrameNativePointer<'_>> {
1301        if self.closed.get() {
1302            Err(closed_handle_error("MetalOwnedTextureFrameHandle"))
1303        } else {
1304            // SAFETY: See texture above.
1305            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) })
1306        }
1307    }
1308
1309    /// Explicitly releases this frame.
1310    #[allow(clippy::result_large_err)]
1311    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1312        self.close_with_release(sys::mln_metal_owned_texture_release_frame)
1313    }
1314
1315    #[allow(clippy::result_large_err)]
1316    fn close_with_release(
1317        self,
1318        release: unsafe extern "C" fn(
1319            sys::mln_render_session,
1320            *const sys::mln_metal_owned_texture_frame,
1321        ) -> sys::mln_status,
1322    ) -> std::result::Result<(), HandleOperationError<Self>> {
1323        if self.closed.get() {
1324            return Ok(());
1325        }
1326        let session = match self.session.native() {
1327            Ok(session) => session,
1328            Err(error) => return Err(HandleOperationError::new(error, self)),
1329        };
1330        // SAFETY: session is live, and raw is the active frame returned by a
1331        // successful acquire for this session until release succeeds.
1332        if let Err(error) = maplibre_core::check(unsafe { release(session, &self.raw) }) {
1333            return Err(HandleOperationError::new(error, self));
1334        }
1335        self.closed.set(true);
1336        self.session.frame_acquired.set(false);
1337        Ok(())
1338    }
1339}
1340
1341impl Drop for MetalOwnedTextureFrameHandle {
1342    fn drop(&mut self) {
1343        if self.closed.get() {
1344            return;
1345        }
1346        if let Ok(session) = self.session.native() {
1347            // SAFETY: Best-effort release of the active frame. Drop cannot
1348            // report errors and never panics.
1349            let status = unsafe { sys::mln_metal_owned_texture_release_frame(session, &self.raw) };
1350            if status == sys::MLN_STATUS_OK {
1351                self.closed.set(true);
1352                self.session.frame_acquired.set(false);
1353            }
1354        }
1355    }
1356}
1357
1358/// RAII guard for an acquired Vulkan session-owned texture frame.
1359///
1360/// Releasing the guard ends the borrow of the backend Vulkan image, image view,
1361/// and device.
1362pub struct VulkanOwnedTextureFrameHandle {
1363    session: Rc<RenderSessionState>,
1364    raw: sys::mln_vulkan_owned_texture_frame,
1365    frame: VulkanOwnedTextureFrame,
1366    closed: Cell<bool>,
1367    _thread_affine: PhantomData<Rc<()>>,
1368}
1369
1370impl fmt::Debug for VulkanOwnedTextureFrameHandle {
1371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1372        f.debug_struct("VulkanOwnedTextureFrameHandle")
1373            .field("closed", &self.closed.get())
1374            .field("frame", &self.frame)
1375            .finish()
1376    }
1377}
1378
1379impl VulkanOwnedTextureFrameHandle {
1380    /// Returns copied metadata for this acquired frame.
1381    pub fn frame(&self) -> Result<&VulkanOwnedTextureFrame> {
1382        if self.closed.get() {
1383            Err(closed_handle_error("VulkanOwnedTextureFrameHandle"))
1384        } else {
1385            Ok(&self.frame)
1386        }
1387    }
1388    /// Returns the borrowed Vulkan image handle for backend interop.
1389    ///
1390    /// # Safety
1391    ///
1392    /// The returned handle is valid only while this frame handle remains open.
1393    /// The caller must not store or use it after frame release and must satisfy
1394    /// Vulkan synchronization and thread-affinity requirements.
1395    pub unsafe fn image(&self) -> Result<FrameVulkanHandle<'_>> {
1396        if self.closed.get() {
1397            Err(closed_handle_error("VulkanOwnedTextureFrameHandle"))
1398        } else {
1399            Ok(FrameVulkanHandle::new(self.raw.image))
1400        }
1401    }
1402
1403    /// Returns the borrowed Vulkan image view handle for backend interop.
1404    ///
1405    /// # Safety
1406    ///
1407    /// The returned handle has the same lifetime and synchronization
1408    /// requirements as [`VulkanOwnedTextureFrameHandle::image`].
1409    pub unsafe fn image_view(&self) -> Result<FrameVulkanHandle<'_>> {
1410        if self.closed.get() {
1411            Err(closed_handle_error("VulkanOwnedTextureFrameHandle"))
1412        } else {
1413            Ok(FrameVulkanHandle::new(self.raw.image_view))
1414        }
1415    }
1416
1417    /// Returns the borrowed Vulkan device pointer for backend interop.
1418    ///
1419    /// # Safety
1420    ///
1421    /// The returned pointer has the same lifetime and synchronization
1422    /// requirements as [`VulkanOwnedTextureFrameHandle::image`].
1423    pub unsafe fn device(&self) -> Result<FrameNativePointer<'_>> {
1424        if self.closed.get() {
1425            Err(closed_handle_error("VulkanOwnedTextureFrameHandle"))
1426        } else {
1427            // SAFETY: See image above.
1428            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) })
1429        }
1430    }
1431
1432    /// Explicitly releases this frame.
1433    #[allow(clippy::result_large_err)]
1434    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1435        if self.closed.get() {
1436            return Ok(());
1437        }
1438        let session = match self.session.native() {
1439            Ok(session) => session,
1440            Err(error) => return Err(HandleOperationError::new(error, self)),
1441        };
1442        // SAFETY: session is live, and raw is the active frame returned by a
1443        // successful acquire for this session until release succeeds.
1444        if let Err(error) = maplibre_core::check(unsafe {
1445            sys::mln_vulkan_owned_texture_release_frame(session, &self.raw)
1446        }) {
1447            return Err(HandleOperationError::new(error, self));
1448        }
1449        self.closed.set(true);
1450        self.session.frame_acquired.set(false);
1451        Ok(())
1452    }
1453}
1454
1455impl Drop for VulkanOwnedTextureFrameHandle {
1456    fn drop(&mut self) {
1457        if self.closed.get() {
1458            return;
1459        }
1460        if let Ok(session) = self.session.native() {
1461            // SAFETY: Best-effort release of the active frame. Drop cannot
1462            // report errors and never panics.
1463            let status = unsafe { sys::mln_vulkan_owned_texture_release_frame(session, &self.raw) };
1464            if status == sys::MLN_STATUS_OK {
1465                self.closed.set(true);
1466                self.session.frame_acquired.set(false);
1467            }
1468        }
1469    }
1470}
1471
1472/// RAII guard for an acquired WebGPU session-owned texture frame.
1473///
1474/// Releasing the guard ends the borrow of the backend WebGPU texture, texture
1475/// view, and device.
1476pub struct WebGpuOwnedTextureFrameHandle {
1477    session: Rc<RenderSessionState>,
1478    raw: sys::mln_webgpu_owned_texture_frame,
1479    frame: WebGpuOwnedTextureFrame,
1480    closed: Cell<bool>,
1481    _thread_affine: PhantomData<Rc<()>>,
1482}
1483
1484impl fmt::Debug for WebGpuOwnedTextureFrameHandle {
1485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1486        f.debug_struct("WebGpuOwnedTextureFrameHandle")
1487            .field("closed", &self.closed.get())
1488            .field("frame", &self.frame)
1489            .finish()
1490    }
1491}
1492
1493impl WebGpuOwnedTextureFrameHandle {
1494    /// Returns copied metadata for this acquired frame.
1495    pub fn frame(&self) -> Result<&WebGpuOwnedTextureFrame> {
1496        if self.closed.get() {
1497            Err(closed_handle_error("WebGpuOwnedTextureFrameHandle"))
1498        } else {
1499            Ok(&self.frame)
1500        }
1501    }
1502
1503    /// Returns the borrowed WebGPU texture pointer for backend interop.
1504    ///
1505    /// # Safety
1506    ///
1507    /// The returned pointer is valid only while this frame handle remains open.
1508    /// The caller must not store or use it after frame release and must satisfy
1509    /// WebGPU synchronization and thread-affinity requirements.
1510    pub unsafe fn texture(&self) -> Result<FrameNativePointer<'_>> {
1511        if self.closed.get() {
1512            Err(closed_handle_error("WebGpuOwnedTextureFrameHandle"))
1513        } else {
1514            // SAFETY: The active native frame owns the validity contract for
1515            // this borrowed backend handle until release.
1516            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture) })
1517        }
1518    }
1519
1520    /// Returns the borrowed WebGPU texture view pointer for backend interop.
1521    ///
1522    /// # Safety
1523    ///
1524    /// The returned pointer has the same lifetime and synchronization
1525    /// requirements as [`WebGpuOwnedTextureFrameHandle::texture`].
1526    pub unsafe fn texture_view(&self) -> Result<FrameNativePointer<'_>> {
1527        if self.closed.get() {
1528            Err(closed_handle_error("WebGpuOwnedTextureFrameHandle"))
1529        } else {
1530            // SAFETY: See texture above.
1531            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture_view) })
1532        }
1533    }
1534
1535    /// Returns the borrowed WebGPU device pointer for backend interop.
1536    ///
1537    /// # Safety
1538    ///
1539    /// The returned pointer has the same lifetime and synchronization
1540    /// requirements as [`WebGpuOwnedTextureFrameHandle::texture`].
1541    pub unsafe fn device(&self) -> Result<FrameNativePointer<'_>> {
1542        if self.closed.get() {
1543            Err(closed_handle_error("WebGpuOwnedTextureFrameHandle"))
1544        } else {
1545            // SAFETY: See texture above.
1546            Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) })
1547        }
1548    }
1549
1550    /// Explicitly releases this frame.
1551    #[allow(clippy::result_large_err)]
1552    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1553        if self.closed.get() {
1554            return Ok(());
1555        }
1556        let session = match self.session.native() {
1557            Ok(session) => session,
1558            Err(error) => return Err(HandleOperationError::new(error, self)),
1559        };
1560        // SAFETY: session is live, and raw is the active frame returned by a
1561        // successful acquire for this session until release succeeds.
1562        if let Err(error) = maplibre_core::check(unsafe {
1563            sys::mln_webgpu_owned_texture_release_frame(session, &self.raw)
1564        }) {
1565            return Err(HandleOperationError::new(error, self));
1566        }
1567        self.closed.set(true);
1568        self.session.frame_acquired.set(false);
1569        Ok(())
1570    }
1571}
1572
1573impl Drop for WebGpuOwnedTextureFrameHandle {
1574    fn drop(&mut self) {
1575        if self.closed.get() {
1576            return;
1577        }
1578        if let Ok(session) = self.session.native() {
1579            // SAFETY: Best-effort release of the active frame. Drop cannot
1580            // report errors and never panics.
1581            let status = unsafe { sys::mln_webgpu_owned_texture_release_frame(session, &self.raw) };
1582            if status == sys::MLN_STATUS_OK {
1583                self.closed.set(true);
1584                self.session.frame_acquired.set(false);
1585            }
1586        }
1587    }
1588}
1589
1590/// RAII guard for an acquired OpenGL session-owned texture frame.
1591///
1592/// Releasing the guard ends the borrow of the backend OpenGL texture object.
1593pub struct OpenGLOwnedTextureFrameHandle {
1594    session: Rc<RenderSessionState>,
1595    raw: sys::mln_opengl_owned_texture_frame,
1596    frame: OpenGLOwnedTextureFrame,
1597    closed: Cell<bool>,
1598    _thread_affine: PhantomData<Rc<()>>,
1599}
1600
1601impl fmt::Debug for OpenGLOwnedTextureFrameHandle {
1602    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1603        f.debug_struct("OpenGLOwnedTextureFrameHandle")
1604            .field("closed", &self.closed.get())
1605            .field("frame", &self.frame)
1606            .finish()
1607    }
1608}
1609
1610impl OpenGLOwnedTextureFrameHandle {
1611    /// Returns copied metadata for this acquired frame.
1612    pub fn frame(&self) -> Result<&OpenGLOwnedTextureFrame> {
1613        if self.closed.get() {
1614            Err(closed_handle_error("OpenGLOwnedTextureFrameHandle"))
1615        } else {
1616            Ok(&self.frame)
1617        }
1618    }
1619    /// Returns the borrowed OpenGL texture object name for backend interop.
1620    pub fn texture(&self) -> Result<FrameOpenGLTextureName<'_>> {
1621        if self.closed.get() {
1622            Err(closed_handle_error("OpenGLOwnedTextureFrameHandle"))
1623        } else {
1624            Ok(FrameOpenGLTextureName::new(self.raw.texture))
1625        }
1626    }
1627
1628    /// Explicitly releases this frame.
1629    #[allow(clippy::result_large_err)]
1630    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1631        if self.closed.get() {
1632            return Ok(());
1633        }
1634        let session = match self.session.native() {
1635            Ok(session) => session,
1636            Err(error) => return Err(HandleOperationError::new(error, self)),
1637        };
1638        // SAFETY: session is live, and raw is the active frame returned by a
1639        // successful acquire for this session until release succeeds.
1640        if let Err(error) = maplibre_core::check(unsafe {
1641            sys::mln_opengl_owned_texture_release_frame(session, &self.raw)
1642        }) {
1643            return Err(HandleOperationError::new(error, self));
1644        }
1645        self.closed.set(true);
1646        self.session.frame_acquired.set(false);
1647        Ok(())
1648    }
1649}
1650
1651impl Drop for OpenGLOwnedTextureFrameHandle {
1652    fn drop(&mut self) {
1653        if self.closed.get() {
1654            return;
1655        }
1656        if let Ok(session) = self.session.native() {
1657            // SAFETY: Best-effort release of the active frame. Drop cannot
1658            // report errors and never panics.
1659            let status = unsafe { sys::mln_opengl_owned_texture_release_frame(session, &self.raw) };
1660            if status == sys::MLN_STATUS_OK {
1661                self.closed.set(true);
1662                self.session.frame_acquired.set(false);
1663            }
1664        }
1665    }
1666}
1667
1668impl RenderSessionHandle {
1669    pub(crate) fn attach<F>(map: &MapAttachRef, attach: F) -> Result<Self>
1670    where
1671        F: FnOnce(sys::mln_map, *mut sys::mln_render_session) -> sys::mln_status,
1672    {
1673        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_render_session>::new();
1674        // A close racing this attach makes the handle stale, which the C API
1675        // rejects.
1676        let status = attach(map.map(), out.as_mut_ptr());
1677        maplibre_core::check(status)?;
1678        let ptr = out_handle(out, "mln_render_session")?;
1679        Ok(Self {
1680            inner: Rc::new(RenderSessionState::new(ptr)?),
1681        })
1682    }
1683
1684    /// Explicitly destroys the render session.
1685    ///
1686    /// Native destruction errors are returned. When destruction fails, the
1687    /// underlying native handle remains live so a later `close` can retry.
1688    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
1689        if let Err(error) = self.inner.ensure_no_frame_acquired() {
1690            return Err(HandleOperationError::new(error, self));
1691        }
1692        if let Err(error) = self.inner.close() {
1693            return Err(HandleOperationError::new(error, self));
1694        }
1695        Ok(())
1696    }
1697    /// Resizes this attached render session.
1698    ///
1699    /// Surface and owned-texture sessions resize in place, keeping the
1700    /// renderer along with the tile pyramid, glyph and image atlases, and
1701    /// symbol placement. A scale factor change retires the renderer instead,
1702    /// because shaders are compiled for one pixel ratio. Map-owned feature
1703    /// state survives either way. Borrowed texture targets report an
1704    /// unsupported-feature error; hand over a new texture with the backend's
1705    /// `set_*_borrowed_texture_target` method instead.
1706    pub fn resize(&self, width: u32, height: u32, scale_factor: f64) -> Result<()> {
1707        self.inner.ensure_no_frame_acquired()?;
1708        let session = self.inner.native()?;
1709        // SAFETY: session is a live render session handle owned by this wrapper.
1710        maplibre_core::check(unsafe {
1711            sys::mln_render_session_resize(session, width, height, scale_factor)
1712        })
1713    }
1714
1715    /// Presents this attached surface session through a new surface, keeping
1716    /// this session's renderer and its state.
1717    ///
1718    /// The descriptor's extent applies as a resize does. A `context.device`
1719    /// that is neither null nor this session's device reports an
1720    /// invalid-argument error and leaves the current surface in place.
1721    pub fn set_metal_surface_target(&self, descriptor: &MetalSurfaceDescriptor) -> Result<()> {
1722        self.set_target(descriptor.to_native(), |session, raw| {
1723            // SAFETY: session is a live render session handle owned by this
1724            // wrapper, and raw is a materialized descriptor valid for this call.
1725            unsafe { sys::mln_metal_surface_set_target(session, raw) }
1726        })
1727    }
1728
1729    /// Presents this attached surface session through a new surface.
1730    ///
1731    /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing
1732    /// a surface preserves. The outgoing `VkSurfaceKHR` must still be valid:
1733    /// this session holds a swapchain built from it, and Vulkan destroys every
1734    /// swapchain before its surface.
1735    pub fn set_vulkan_surface_target(&self, descriptor: &VulkanSurfaceDescriptor) -> Result<()> {
1736        self.set_target(descriptor.to_native(), |session, raw| {
1737            // SAFETY: session is a live render session handle owned by this
1738            // wrapper, and raw is a materialized descriptor valid for this call.
1739            unsafe { sys::mln_vulkan_surface_set_target(session, raw) }
1740        })
1741    }
1742
1743    /// Presents this attached surface session through a new WebGPU surface.
1744    ///
1745    /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing
1746    /// a surface preserves. The replacement names the same device and format as
1747    /// the session attached with.
1748    pub fn set_webgpu_surface_target(&self, descriptor: &WebGpuSurfaceDescriptor) -> Result<()> {
1749        self.set_target(descriptor.to_native(), |session, raw| {
1750            // SAFETY: session is a live render session handle owned by this
1751            // wrapper, and raw is a materialized descriptor valid for this call.
1752            unsafe { sys::mln_webgpu_surface_set_target(session, raw) }
1753        })
1754    }
1755
1756    /// Presents this attached surface session through a new surface.
1757    ///
1758    /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing
1759    /// a surface preserves. The new surface is made current on the next render,
1760    /// so a host may hand over a replacement for one it has already destroyed.
1761    /// A surface accepted here can still prove unusable, which the next
1762    /// `render_update` reports rather than this call.
1763    pub fn set_opengl_surface_target(&self, descriptor: &OpenGLSurfaceDescriptor) -> Result<()> {
1764        self.set_target(descriptor.to_native(), |session, raw| {
1765            // SAFETY: session is a live render session handle owned by this
1766            // wrapper, and raw is a materialized descriptor valid for this call.
1767            unsafe { sys::mln_opengl_surface_set_target(session, raw) }
1768        })
1769    }
1770
1771    /// Renders this attached texture session into a new caller-owned texture,
1772    /// keeping this session's renderer. A scale factor change starts a new
1773    /// renderer, as [`RenderSessionHandle::resize`] does.
1774    ///
1775    /// The replacement must belong to the device this session attached with and
1776    /// carry the pixel format it attached with; otherwise this reports an error
1777    /// and leaves the current texture in place. The caller owns the replacement
1778    /// and keeps it valid until the next replacement, detach, or close. The
1779    /// outgoing texture is neither retained nor read here.
1780    pub fn set_metal_borrowed_texture_target(
1781        &self,
1782        descriptor: &MetalBorrowedTextureDescriptor,
1783    ) -> Result<()> {
1784        self.set_target(descriptor.to_native(), |session, raw| {
1785            // SAFETY: session is a live render session handle owned by this
1786            // wrapper, and raw is a materialized descriptor valid for this call.
1787            unsafe { sys::mln_metal_borrowed_texture_set_target(session, raw) }
1788        })
1789    }
1790
1791    /// Renders this attached texture session into a new caller-owned image.
1792    ///
1793    /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what
1794    /// replacing a target preserves. The replacement carries the format and
1795    /// both layouts this session attached with, since its render pass was built
1796    /// around them.
1797    pub fn set_vulkan_borrowed_texture_target(
1798        &self,
1799        descriptor: &VulkanBorrowedTextureDescriptor,
1800    ) -> Result<()> {
1801        self.set_target(descriptor.to_native(), |session, raw| {
1802            // SAFETY: session is a live render session handle owned by this
1803            // wrapper, and raw is a materialized descriptor valid for this call.
1804            unsafe { sys::mln_vulkan_borrowed_texture_set_target(session, raw) }
1805        })
1806    }
1807
1808    /// Renders this attached texture session into a new caller-owned texture.
1809    ///
1810    /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what
1811    /// replacing a target preserves. The replacement carries the format this
1812    /// session attached with, and belongs to the device it attached with.
1813    pub fn set_webgpu_borrowed_texture_target(
1814        &self,
1815        descriptor: &WebGpuBorrowedTextureDescriptor,
1816    ) -> Result<()> {
1817        self.set_target(descriptor.to_native(), |session, raw| {
1818            // SAFETY: session is a live render session handle owned by this
1819            // wrapper, and raw is a materialized descriptor valid for this call.
1820            unsafe { sys::mln_webgpu_borrowed_texture_set_target(session, raw) }
1821        })
1822    }
1823
1824    /// Renders this attached texture session into a new caller-owned texture.
1825    ///
1826    /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what
1827    /// replacing a target preserves. The replacement belongs to the context
1828    /// this session attached with, or one in its share group, and the host
1829    /// context must be current on this thread.
1830    pub fn set_opengl_borrowed_texture_target(
1831        &self,
1832        descriptor: &OpenGLBorrowedTextureDescriptor,
1833    ) -> Result<()> {
1834        self.set_target(descriptor.to_native(), |session, raw| {
1835            // SAFETY: session is a live render session handle owned by this
1836            // wrapper, and raw is a materialized descriptor valid for this call.
1837            unsafe { sys::mln_opengl_borrowed_texture_set_target(session, raw) }
1838        })
1839    }
1840
1841    /// Shared body for the `set_*_target` methods. The caller-materialized
1842    /// descriptor lives for the whole call, which is all the C API borrows it
1843    /// for.
1844    fn set_target<D>(
1845        &self,
1846        raw: D,
1847        set_target: impl FnOnce(sys::mln_render_session, &D) -> sys::mln_status,
1848    ) -> Result<()> {
1849        self.inner.ensure_no_frame_acquired()?;
1850        let session = self.inner.native()?;
1851        maplibre_core::check(set_target(session, &raw))
1852    }
1853
1854    /// Processes the latest map render update for this render target.
1855    ///
1856    /// The map retains its latest update, so repeated calls re-render it and
1857    /// report [`RenderResult::Rendered`] again. Every other result names the
1858    /// wake to wait for: [`RenderResult::NoUpdate`] and
1859    /// [`RenderResult::SizePending`] resolve on a render-update-available
1860    /// event, and [`RenderResult::TargetNotReady`] resolves when the host
1861    /// changes the render target.
1862    ///
1863    /// The returned [`RenderUpdate::needs_repaint`] reports whether the map
1864    /// asked for another frame while rendering this one, so a frame loop can
1865    /// re-arm without draining events first.
1866    pub fn render_update(&self) -> Result<RenderUpdate> {
1867        self.inner.ensure_no_frame_acquired()?;
1868        let session = self.inner.native()?;
1869        let mut result = sys::MLN_RENDER_RESULT_NO_UPDATE;
1870        let mut needs_repaint = false;
1871        // SAFETY: session is a live render session handle owned by this wrapper,
1872        // and result and needs_repaint point to caller-owned output storage.
1873        maplibre_core::check(unsafe {
1874            sys::mln_render_session_render_update(session, &raw mut result, &raw mut needs_repaint)
1875        })?;
1876        Ok(RenderUpdate {
1877            result: RenderResult::from_raw(result),
1878            needs_repaint,
1879        })
1880    }
1881
1882    /// Detaches backend-bound render resources from the map, consuming this
1883    /// handle and returning a close-only handle.
1884    ///
1885    /// A detached session no longer reaches its map, so the map is free to
1886    /// close and the detached session stays destroyable afterwards.
1887    pub fn detach(
1888        self,
1889    ) -> std::result::Result<DetachedRenderSessionHandle, HandleOperationError<Self>> {
1890        if let Err(error) = self.inner.ensure_no_frame_acquired() {
1891            return Err(HandleOperationError::new(error, self));
1892        }
1893        let session = match self.inner.native() {
1894            Ok(session) => session,
1895            Err(error) => return Err(HandleOperationError::new(error, self)),
1896        };
1897        // SAFETY: session is a live render session handle owned by this wrapper.
1898        if let Err(error) = maplibre_core::check(unsafe { sys::mln_render_session_detach(session) })
1899        {
1900            return Err(HandleOperationError::new(error, self));
1901        }
1902        self.inner.detached.set(true);
1903        Ok(DetachedRenderSessionHandle {
1904            inner: Rc::clone(&self.inner),
1905        })
1906    }
1907
1908    /// Asks the session renderer to release cached resources where possible.
1909    pub fn reduce_memory_use(&self) -> Result<()> {
1910        self.inner.ensure_no_frame_acquired()?;
1911        let session = self.inner.native()?;
1912        // SAFETY: session is a live render session handle owned by this wrapper.
1913        maplibre_core::check(unsafe { sys::mln_render_session_reduce_memory_use(session) })
1914    }
1915
1916    /// Clears renderer data for the session.
1917    pub fn clear_data(&self) -> Result<()> {
1918        self.inner.ensure_no_frame_acquired()?;
1919        let session = self.inner.native()?;
1920        // SAFETY: session is a live render session handle owned by this wrapper.
1921        maplibre_core::check(unsafe { sys::mln_render_session_clear_data(session) })
1922    }
1923
1924    /// Dumps renderer debug logs through MapLibre Native logging.
1925    pub fn dump_debug_logs(&self) -> Result<()> {
1926        self.inner.ensure_no_frame_acquired()?;
1927        let session = self.inner.native()?;
1928        // SAFETY: session is a live render session handle owned by this wrapper.
1929        maplibre_core::check(unsafe { sys::mln_render_session_dump_debug_logs(session) })
1930    }
1931
1932    /// Returns CPU readback metadata for the most recently rendered texture frame.
1933    pub fn texture_image_info(&self) -> Result<TextureImageInfo> {
1934        self.inner.ensure_no_frame_acquired()?;
1935        let session = self.inner.native()?;
1936        // SAFETY: Default constructor takes no arguments and initializes size.
1937        let mut info = unsafe { sys::mln_texture_image_info_default() };
1938        // SAFETY: session is live. Passing a null buffer with zero capacity is
1939        // the documented metadata probe path; out_info points to initialized storage.
1940        maplibre_core::check(unsafe {
1941            sys::mln_texture_read_premultiplied_rgba8(session, std::ptr::null_mut(), 0, &mut info)
1942        })?;
1943        Ok(maplibre_core::values::texture_image_info_from_native(&info))
1944    }
1945
1946    /// Reads the most recently rendered texture frame as premultiplied RGBA8.
1947    pub fn read_premultiplied_rgba8_into(&self, data: &mut [u8]) -> Result<TextureImageInfo> {
1948        self.inner.ensure_no_frame_acquired()?;
1949        let session = self.inner.native()?;
1950        // SAFETY: Default constructor takes no arguments and initializes size.
1951        let mut info = unsafe { sys::mln_texture_image_info_default() };
1952        let data_ptr = if data.is_empty() {
1953            std::ptr::null_mut()
1954        } else {
1955            data.as_mut_ptr()
1956        };
1957        // SAFETY: session is live, data_ptr either points to data's mutable
1958        // storage for data.len() bytes or is null for an empty buffer, and info
1959        // points to initialized writable storage.
1960        maplibre_core::check(unsafe {
1961            sys::mln_texture_read_premultiplied_rgba8(session, data_ptr, data.len(), &mut info)
1962        })?;
1963        // An empty destination reaches native code as the size probe, which
1964        // succeeds without copying, so report it as too small here.
1965        if data.is_empty() && info.byte_length > 0 {
1966            return Err(crate::Error::invalid_argument(format!(
1967                "buffer length 0 is smaller than the required {} bytes",
1968                info.byte_length
1969            )));
1970        }
1971        Ok(maplibre_core::values::texture_image_info_from_native(&info))
1972    }
1973
1974    /// Acquires a borrowed Metal frame from a session-owned texture target.
1975    pub fn acquire_metal_owned_texture_frame(&self) -> Result<MetalOwnedTextureFrameHandle> {
1976        self.inner.ensure_no_frame_acquired()?;
1977        let session = self.inner.native()?;
1978        let mut raw = empty_metal_owned_texture_frame();
1979        // SAFETY: session is live and raw points to initialized writable frame storage.
1980        maplibre_core::check(unsafe {
1981            sys::mln_metal_owned_texture_acquire_frame(session, &mut raw)
1982        })?;
1983        self.inner.frame_acquired.set(true);
1984        Ok(MetalOwnedTextureFrameHandle {
1985            session: Rc::clone(&self.inner),
1986            frame: MetalOwnedTextureFrame::from_native(&raw),
1987            raw,
1988            closed: Cell::new(false),
1989            _thread_affine: PhantomData,
1990        })
1991    }
1992
1993    /// Acquires a borrowed Vulkan frame from a session-owned texture target.
1994    pub fn acquire_vulkan_owned_texture_frame(&self) -> Result<VulkanOwnedTextureFrameHandle> {
1995        self.inner.ensure_no_frame_acquired()?;
1996        let session = self.inner.native()?;
1997        let mut raw = empty_vulkan_owned_texture_frame();
1998        // SAFETY: session is live and raw points to initialized writable frame storage.
1999        maplibre_core::check(unsafe {
2000            sys::mln_vulkan_owned_texture_acquire_frame(session, &mut raw)
2001        })?;
2002        self.inner.frame_acquired.set(true);
2003        Ok(VulkanOwnedTextureFrameHandle {
2004            session: Rc::clone(&self.inner),
2005            frame: VulkanOwnedTextureFrame::from_native(&raw),
2006            raw,
2007            closed: Cell::new(false),
2008            _thread_affine: PhantomData,
2009        })
2010    }
2011
2012    /// Acquires a borrowed WebGPU frame from a session-owned texture target.
2013    pub fn acquire_webgpu_owned_texture_frame(&self) -> Result<WebGpuOwnedTextureFrameHandle> {
2014        self.inner.ensure_no_frame_acquired()?;
2015        let session = self.inner.native()?;
2016        let mut raw = empty_webgpu_owned_texture_frame();
2017        // SAFETY: session is live and raw points to initialized writable frame storage.
2018        maplibre_core::check(unsafe {
2019            sys::mln_webgpu_owned_texture_acquire_frame(session, &mut raw)
2020        })?;
2021        self.inner.frame_acquired.set(true);
2022        Ok(WebGpuOwnedTextureFrameHandle {
2023            session: Rc::clone(&self.inner),
2024            frame: WebGpuOwnedTextureFrame::from_native(&raw),
2025            raw,
2026            closed: Cell::new(false),
2027            _thread_affine: PhantomData,
2028        })
2029    }
2030
2031    /// Acquires a borrowed OpenGL frame from a session-owned texture target.
2032    pub fn acquire_opengl_owned_texture_frame(&self) -> Result<OpenGLOwnedTextureFrameHandle> {
2033        self.inner.ensure_no_frame_acquired()?;
2034        let session = self.inner.native()?;
2035        let mut raw = empty_opengl_owned_texture_frame();
2036        // SAFETY: session is live and raw points to initialized writable frame storage.
2037        maplibre_core::check(unsafe {
2038            sys::mln_opengl_owned_texture_acquire_frame(session, &mut raw)
2039        })?;
2040        self.inner.frame_acquired.set(true);
2041        Ok(OpenGLOwnedTextureFrameHandle {
2042            session: Rc::clone(&self.inner),
2043            frame: OpenGLOwnedTextureFrame::from_native(&raw),
2044            raw,
2045            closed: Cell::new(false),
2046            _thread_affine: PhantomData,
2047        })
2048    }
2049}
2050
2051fn empty_metal_owned_texture_frame() -> sys::mln_metal_owned_texture_frame {
2052    sys::mln_metal_owned_texture_frame {
2053        size: mem::size_of::<sys::mln_metal_owned_texture_frame>() as u32,
2054        generation: 0,
2055        width: 0,
2056        height: 0,
2057        scale_factor: 0.0,
2058        frame_id: 0,
2059        texture: std::ptr::null_mut(),
2060        device: std::ptr::null_mut(),
2061        pixel_format: 0,
2062    }
2063}
2064
2065fn empty_webgpu_owned_texture_frame() -> sys::mln_webgpu_owned_texture_frame {
2066    sys::mln_webgpu_owned_texture_frame {
2067        size: mem::size_of::<sys::mln_webgpu_owned_texture_frame>() as u32,
2068        generation: 0,
2069        width: 0,
2070        height: 0,
2071        scale_factor: 0.0,
2072        frame_id: 0,
2073        texture: std::ptr::null_mut(),
2074        texture_view: std::ptr::null_mut(),
2075        device: std::ptr::null_mut(),
2076        format: 0,
2077    }
2078}
2079
2080fn empty_vulkan_owned_texture_frame() -> sys::mln_vulkan_owned_texture_frame {
2081    sys::mln_vulkan_owned_texture_frame {
2082        size: mem::size_of::<sys::mln_vulkan_owned_texture_frame>() as u32,
2083        generation: 0,
2084        width: 0,
2085        height: 0,
2086        scale_factor: 0.0,
2087        frame_id: 0,
2088        image: 0,
2089        image_view: 0,
2090        device: std::ptr::null_mut(),
2091        format: 0,
2092        layout: 0,
2093    }
2094}
2095
2096fn empty_opengl_owned_texture_frame() -> sys::mln_opengl_owned_texture_frame {
2097    sys::mln_opengl_owned_texture_frame {
2098        size: mem::size_of::<sys::mln_opengl_owned_texture_frame>() as u32,
2099        generation: 0,
2100        width: 0,
2101        height: 0,
2102        scale_factor: 0.0,
2103        frame_id: 0,
2104        texture: 0,
2105        target: 0,
2106        internal_format: 0,
2107        format: 0,
2108        type_: 0,
2109    }
2110}
2111
2112#[cfg(test)]
2113mod tests;