Render to a window
This guide shows how to present a continuous map in a native window or surface, and keep it updated as the map changes. Start with a runtime and a continuous map from Create a map.
Attach a surface
Section titled “Attach a surface”First, create the window with your platform or windowing library, and get the surface handle for your backend:
- Metal: a
CAMetalLayer - Vulkan: a
VkSurfaceKHR - OpenGL: an
EGLSurfacewith EGL, or anHDCwith WGL - WebGL: a
<canvas>, either through a WebGL context that you create on it withemscripten_webgl_create_context, or by its selector, which the library transfers to a worker - WebGPU: a
WGPUSurface, which a browser creates from a<canvas>
On Vulkan, the session renders with your instance, device, and graphics queue.
To have the library create them instead, call mln_vulkan_device_createVulkanDeviceHandle.createVulkanDeviceHandle.CreateNewVulkanDevicemaplibre_native_ffi_vulkan_device_createVulkanDeviceHandle.createVulkanDeviceHandle.createVulkanDeviceHandle.createVulkanDeviceHandle::createVulkanDeviceHandle.createVulkanDeviceHandle.createVulkanDevice.create
with mln_vulkan_device_options.presentationVulkanDeviceOptions.presentationVulkanDeviceOptions.PresentationVulkanDeviceOptions.PresentationMaplibreNativeFfiVulkanDeviceOptions:presentationVulkanDeviceOptions.presentationVulkanDeviceOptions.presentationVulkanDeviceOptions.presentationVulkanDeviceOptions::presentationVulkanDeviceOptions.presentationVulkanDeviceOptions.presentationVulkanDeviceOptions.presentation set. Then create your
surface on the instance of the context from
mln_vulkan_device_get_contextVulkanDeviceHandle.getContextVulkanDeviceHandle.GetContextVulkanDeviceHandle.Contextmaplibre_native_ffi_vulkan_device_get_contextVulkanDeviceHandle.getContextVulkanDeviceHandle.getContextVulkanDeviceHandle.get_contextVulkanDeviceHandle::get_contextVulkanDeviceHandle.getContextVulkanDeviceHandle.getContextVulkanDevice.getContext, and attach with that context.
On Android, attach the window itself with
mln_map_attach_android_surfaceMapHandle.attachAndroidSurfaceMapHandle.AttachAndroidSurfacemln_map_attach_android_surfacemln_map_attach_android_surfaceMapHandle.attachAndroidSurfaceMapHandle.attachAndroidSurfaceMapHandle.attach_android_surfacemln_map_attach_android_surfacemln_map_attach_android_surfaceMapHandle.attachAndroidSurfaceMap.attachAndroidSurface, by its ANativeWindow or its Java
Surface, and the library creates the graphics context. Replace the window with
mln_render_session_set_android_surface_targetRenderSessionHandle.setAndroidSurfaceTargetRenderSessionHandle.SetAndroidSurfaceTargetAsyncmln_render_session_set_android_surface_targetmln_render_session_set_android_surface_targetRenderSessionHandle.setAndroidSurfaceTargetRenderSessionHandle.setAndroidSurfaceTargetRenderSessionHandle.set_android_surface_targetmln_render_session_set_android_surface_targetmln_render_session_set_android_surface_targetRenderSessionHandle.setAndroidSurfaceTargetRenderSession.setAndroidSurfaceTarget. Release it when
surfaceDestroyed runs, as Handle a lost surface
shows.
On OpenHarmony, attach an XComponent’s window the same way with
mln_map_attach_ohos_surfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfaceMapHandle.AttachOhosSurfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfacemln_map_attach_ohos_surfaceMapHandle.attachOhosSurfaceMap.attachOhosSurface, by its OHNativeWindow or its surface ID,
and replace it with mln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetRenderSessionHandle.SetOhosSurfaceTargetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetmln_render_session_set_ohos_surface_targetRenderSessionHandle.setOhosSurfaceTargetRenderSession.setOhosSurfaceTarget. Release
it when the component’s onSurfaceDestroyed runs.
On Linux and FreeBSD, attach a Wayland wl_surface and its wl_display the
same way with mln_map_attach_wayland_surfaceMapHandle.attachWaylandSurfaceMapHandle.AttachWaylandSurfaceMapHandle.AttachWaylandSurfacemaplibre_native_ffi_map_attach_wayland_surfaceMapHandle.attachWaylandSurfaceMapHandle.attachWaylandSurfaceMapHandle.attach_wayland_surfaceMapHandle::attach_wayland_surfaceMapHandle.attachWaylandSurfaceMapHandle.attachWaylandSurfaceMap.attachWaylandSurface, and replace it with
mln_render_session_set_wayland_surface_targetRenderSessionHandle.setWaylandSurfaceTargetRenderSessionHandle.SetWaylandSurfaceTargetAsyncRenderSessionHandle.SetWaylandSurfaceTargetmaplibre_native_ffi_render_session_set_wayland_surface_target_asyncRenderSessionHandle.setWaylandSurfaceTargetRenderSessionHandle.setWaylandSurfaceTargetRenderSessionHandle.set_wayland_surface_targetRenderSessionHandle::set_wayland_surface_targetRenderSessionHandle.setWaylandSurfaceTargetRenderSessionHandle.setWaylandSurfaceTargetRenderSession.setWaylandSurfaceTarget. Release the target before
you destroy the surface, and detach the session before you disconnect the
display.
On Windows, attach a window by its HWND the same way with
mln_map_attach_win32_windowMapHandle.attachWin32WindowMapHandle.AttachWin32Windowmln_map_attach_win32_windowmln_map_attach_win32_windowMapHandle.attachWin32WindowMapHandle.attachWin32WindowMapHandle.attach_win32_windowmln_map_attach_win32_windowmln_map_attach_win32_windowMapHandle.attachWin32WindowMap.attachWin32Window, and replace it with
mln_render_session_set_win32_window_targetRenderSessionHandle.setWin32WindowTargetRenderSessionHandle.SetWin32WindowTargetAsyncmln_render_session_set_win32_window_targetmln_render_session_set_win32_window_targetRenderSessionHandle.setWin32WindowTargetRenderSessionHandle.setWin32WindowTargetRenderSessionHandle.set_win32_window_targetmln_render_session_set_win32_window_targetmln_render_session_set_win32_window_targetRenderSessionHandle.setWin32WindowTargetRenderSession.setWin32WindowTarget. Release the target before
you destroy the window.
Attach the surface to the map at the window’s size in logical pixels. The session renders at the scale factor that you created the map with. To move the window to a display of another density, create a new map and attach a new session. You also pass a redraw wake and a frame wake here, covered in the next section.
The mln_render_session_attach_options.driverRenderSessionAttachOptions.driverRenderSessionAttachOptions.DriverRenderSessionAttachOptions.DriverMaplibreNativeFfiRenderSessionAttachOptions:driverRenderSessionAttachOptions.driverRenderSessionAttachOptions.driverRenderSessionAttachOptions.driverRenderSessionAttachOptions::driverRenderSessionAttachOptions.driverRenderSessionAttachOptions.driverRenderSessionAttachOptions.driver picks which thread runs the
session’s graphics work. Usually that’s the library’s own thread. If the surface
uses your OpenGL or WebGPU context, use your graphics thread instead, since that
context belongs to your thread. See Service driver work.
static mln_render_session session = MLN_HANDLE_NULL;
const mln_vulkan_surface_descriptor surface = { .size = sizeof(mln_vulkan_surface_descriptor), // The window's size in logical pixels. The map's scale factor // turns it into pixels. .logical_size = {.width = 800, .height = 600}, // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. .context = vulkan, // Your VkSurfaceKHR. .surface = window_surface,};const mln_render_session_attach_options options = { .size = sizeof(mln_render_session_attach_options), .driver = MLN_RENDER_DRIVER_CORE_WORKER, .redraw_wake = {.callback = on_redraw_wake}, .frame_wake = {.callback = on_frame_wake},};
mln_map_attach_vulkan_surface( map, &surface, &options, &session, &done, NULL);late RenderSessionHandle session;
session = map.attachVulkanSurface( VulkanSurfaceDescriptor( // The window's size in logical pixels. The map's scale factor // turns it into pixels. logicalSize: const LogicalSize(width: 800, height: 600), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. context: vulkan, // Your VkSurfaceKHR. surface: windowSurface, ), const RenderSessionAttachOptions( driver: RenderDriverKind.coreWorker, redrawWake: Wake(callback: onRedrawWake), frameWake: Wake(callback: onFrameWake), ),);await session.ready;private static RenderSessionHandle session = null!;
session = map.AttachVulkanSurface( new VulkanSurfaceDescriptor { // The window's size in logical // pixels. The map's scale factor // turns it into pixels. LogicalSize = new LogicalSize(800, 600), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. Context = vulkan, // Your VkSurfaceKHR. Surface = windowSurface, }, new RenderSessionAttachOptions { Driver = RenderDriverKind.CoreWorker, RedrawWake = new Wake(OnRedrawWake), FrameWake = new Wake(OnFrameWake), });await session.Ready;var session *maplibre.RenderSessionHandle
surface := maplibre.VulkanSurfaceDescriptor{ // The window's size in logical pixels. The map's scale factor // turns it into pixels. LogicalSize: maplibre.LogicalSize{Width: 800, Height: 600}, // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. Context: vulkan, // Your VkSurfaceKHR. Surface: windowSurface,}options := maplibre.RenderSessionAttachOptions{ Driver: maplibre.RenderDriverKindCoreWorker, RedrawWake: maplibre.Wake{Callback: onRedrawWake}, FrameWake: maplibre.Wake{Callback: onFrameWake},}
attached, ready, err := m.AttachVulkanSurface(surface, options)if err != nil { return err}session = attachedif _, err := ready.Await(ctx); err != nil { return err}RenderSession session;
var descriptor = new VulkanSurfaceDescriptor () { // The window's size in logical pixels. The map's scale // factor turns it into pixels. logical_size = { width: 800, height: 600 }, // Your VkInstance, VkPhysicalDevice, VkDevice, and graphics // VkQueue. context = vulkan, // Your VkSurfaceKHR. surface = window_surface,};
var options = new RenderSessionAttachOptions () { driver = RenderDriverKind.CORE_WORKER, redraw_wake = wake (on_redraw_wake), frame_wake = wake (on_frame_wake),};
session = map.attach_vulkan_surface (descriptor, options);yield session.ready_async ();private RenderSessionHandle session;
session = MaplibreNative.attachVulkanSurface( map, VulkanSurfaceDescriptor.of( // The window's size in logical pixels. The map's // scale factor turns it into pixels. LogicalSize.of(800, 600), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. vulkan, // Your VkSurfaceKHR. windowSurface), RenderSessionAttachOptions.of( RenderDriverKind.CORE_WORKER) .withRedrawWake(new Wake(this::onRedrawWake)) .withFrameWake(new Wake(this::onFrameWake)));session.getReady().join();private lateinit var session: RenderSessionHandle
session = map.attachVulkanSurface( VulkanSurfaceDescriptor( // The window's size in logical pixels. The map's scale factor // turns it into pixels. logicalSize = LogicalSize(width = 800u, height = 600u), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. context = vulkan, // Your VkSurfaceKHR. surface = windowSurface, ), RenderSessionAttachOptions( driver = RenderDriverKind.CORE_WORKER, redrawWake = Wake(::onRedrawWake), frameWake = Wake(::onFrameWake), ), )session.ready.await()session: mln.RenderSessionHandle
session = map_handle.attach_vulkan_surface( mln.VulkanSurfaceDescriptor( # The window's size in logical pixels. The map's # scale factor turns it into pixels. logical_size=mln.LogicalSize(width=800, height=600), # Your VkInstance, VkPhysicalDevice, # VkDevice, and graphics VkQueue. context=vulkan, # Your VkSurfaceKHR. surface=window_surface, ), mln.RenderSessionAttachOptions( driver=mln.RenderDriverKind.CORE_WORKER, redraw_wake=mln.Wake(on_redraw_wake), frame_wake=mln.Wake(on_frame_wake), ),)session.ready.result()struct MapWindow { session: RenderSessionHandle,}
let surface = VulkanSurfaceDescriptor { // The window's size in logical pixels. The map's scale factor // turns it into pixels. logical_size: LogicalSize::new(800, 600), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. context: vulkan, // Your VkSurfaceKHR. surface: window_surface,};let options = RenderSessionAttachOptions { redraw_wake: Wake::new(on_redraw_wake), frame_wake: Wake::new(on_frame_wake), ..RenderSessionAttachOptions::new( RenderDriverKind::CoreWorker, )};
// SAFETY: the surface and the Vulkan objects outlive the session.let (session, attached) = unsafe { map.attach_vulkan_surface(&surface, &options)? };attached.await?;
let mut window = MapWindow { session };@MainActor private var session: RenderSessionHandle!
session = try map.attachVulkanSurface( descriptor: VulkanSurfaceDescriptor( // The window's size in logical pixels. The map's scale factor // turns it into pixels. logicalSize: LogicalSize(width: 800, height: 600), // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. context: vulkan, // Your VkSurfaceKHR. surface: windowSurface ), options: RenderSessionAttachOptions( driver: .coreWorker, frameWake: Wake(callback: onFrameWake), redrawWake: Wake(callback: onRedrawWake) ))try await session.readylet session: RenderSessionHandle;
session = map.attachVulkanSurface( { // The window's size in logical pixels. The map's scale factor turns // it into pixels. logicalSize: { width: 800, height: 600 }, // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. context: vulkan, // Your VkSurfaceKHR. surface: windowSurface, }, { driver: RenderDriverKind.coreWorker, redrawWake: { callback: onRedrawWake }, frameWake: { callback: onFrameWake }, },);await session.ready;var session: maplibre.RenderSession = undefined;
const attached = try map.attachVulkanSurface(.{ // The window's size in logical pixels. The map's scale factor // turns it into pixels. .logical_size = .{ .width = 800, .height = 600 }, // Your VkInstance, VkPhysicalDevice, // VkDevice, and graphics VkQueue. .context = vulkan, // Your VkSurfaceKHR. .surface = window_surface,}, .{ .driver = .core_worker, .redraw_wake = .{ .callback = onRedrawWake }, .frame_wake = .{ .callback = onFrameWake },}, null);session = attached.session;try attached.ready.await(null);Render frames
Section titled “Render frames”The map updates on the runtime’s thread whenever something changes, like the camera moving or a tile loading. Your app decides when to show those updates, at most once per display refresh.
The mln_render_session_attach_options.redraw_wakeRenderSessionAttachOptions.redrawWakeRenderSessionAttachOptions.RedrawWakeRenderSessionAttachOptions.RedrawWakeMaplibreNativeFfiRenderSessionAttachOptions:redraw-wakeRenderSessionAttachOptions.redrawWakeRenderSessionAttachOptions.redrawWakeRenderSessionAttachOptions.redraw_wakeRenderSessionAttachOptions::redraw_wakeRenderSessionAttachOptions.redrawWakeRenderSessionAttachOptions.redrawWakeRenderSessionAttachOptions.redraw_wake tells you that the
session has something new to draw. At your next display refresh, call
mln_render_session_demand_frameRenderSessionHandle.demandFrameRenderSessionHandle.DemandFrameRenderSessionHandle.DemandFramemaplibre_native_ffi_render_session_demand_frameRenderSessionHandle.demandFrameRenderSessionHandle.demandFrameRenderSessionHandle.demand_frameRenderSessionHandle::demand_frameRenderSessionHandle.demandFrameRenderSessionHandle.demandFrameRenderSession.demandFrame without flags. The session draws the
change and presents it. The redraw wake isn’t called again until you demand a
frame, so a map that doesn’t change costs nothing.
A frame shows every command that you sent before you demanded it.
sequenceDiagram
accTitle: The frame loop
participant App as Your event loop
participant Session as Render session
loop Whenever the map changes
Session-->>App: Redraw wake
Note over App: Wait for the next display refresh
App->>Session: Demand a frame
Session->>Session: Draw and present the change
Session-->>App: Frame wake
App->>Session: Drain the frame results
endThe mln_render_session_attach_options.frame_wakeRenderSessionAttachOptions.frameWakeRenderSessionAttachOptions.FrameWakeRenderSessionAttachOptions.FrameWakeMaplibreNativeFfiRenderSessionAttachOptions:frame-wakeRenderSessionAttachOptions.frameWakeRenderSessionAttachOptions.frameWakeRenderSessionAttachOptions.frame_wakeRenderSessionAttachOptions::frame_wakeRenderSessionAttachOptions.frameWakeRenderSessionAttachOptions.frameWakeRenderSessionAttachOptions.frame_wake tells you that results
are ready. Both wakes can run on a library thread, so have them schedule work on
your event loop: the demand at the next refresh, and a drain of the results with
mln_render_session_drain_frame_resultsRenderSessionHandle.drainFrameResultsRenderSessionHandle.DrainFrameResultsRenderSessionHandle.DrainFrameResultsmaplibre_native_ffi_render_session_drain_frame_resultsRenderSessionHandle.drainFrameResultsRenderSessionHandle.drainFrameResultsRenderSessionHandle.drain_frame_resultsRenderSessionHandle::drain_frame_resultsRenderSessionHandle.drainFrameResultsRenderSessionHandle.drainFrameResultsRenderSession.drainFrameResults.
// Both run on a library thread.static void on_redraw_wake(void* user_data) { (void)user_data; run_at_next_refresh(demand_frame);}
static void on_frame_wake(void* user_data) { (void)user_data; run_on_event_loop(drain_frame_results);}// The binding calls both wakes in this isolate,// so the frame wake can drain right away.void onRedrawWake() { atNextRefresh(demandFrame);}
void onFrameWake() { drainFrameResults();}// Both run on a library thread.private static void OnRedrawWake() => RunAtNextRefresh(DemandFrame);
private static void OnFrameWake() => RunOnEventLoop(DrainFrameResults);// Both run on a library thread.func onRedrawWake() { runAtNextRefresh(demandFrame)}
func onFrameWake() { runOnEventLoop(drainFrameResults)}// Both run on the main context of the thread that attached the// session.void on_redraw_wake () { run_at_next_refresh (demand_frame);}
void on_frame_wake () { try { drain_frame_results (); } catch (GLib.Error error) { warning ("%s", error.message); }}// Both run on a library thread.private void onRedrawWake() { runAtNextRefresh(this::demandFrame);}
private void onFrameWake() { runOnEventLoop(this::drainFrameResults);}// Both run on a library thread.private fun onRedrawWake() { runAtNextRefresh(::demandFrame)}
private fun onFrameWake() { runOnEventLoop(::drainFrameResults)}# Both run on the binding's dispatcher thread.def on_redraw_wake() -> None: run_at_next_refresh(demand_frame)
def on_frame_wake() -> None: run_on_event_loop(drain_frame_results)// Both run on a library thread.fn on_redraw_wake() { run_at_next_refresh(MapWindow::demand_frame);}
fn on_frame_wake() { run_on_event_loop(MapWindow::drain_frame_results);}/// Both run on a library thread.private func onRedrawWake() { atNextRefresh(demandFrame)}
private func onFrameWake() { runOnEventLoop(drainFrameResults)}// The binding calls both wakes on the JS thread,// so the frame wake can drain right away.function onRedrawWake(): void { atNextRefresh(demandFrame);}
function onFrameWake(): void { drainFrameResults();}// Both run on a library thread.fn onRedrawWake(_: ?*anyopaque) anyerror!void { runAtNextRefresh(demandFrame);}
fn onFrameWake(_: ?*anyopaque) anyerror!void { runOnEventLoop(drainFrameResults);}If a result is MLN_RENDER_RESULT_FAILEDRenderResult.failedRenderResult.FailedRenderResultFailedMAPLIBRE_NATIVE_FFI_RENDER_RESULT_FAILEDRenderResult.FAILEDRenderResult.FAILEDRenderResult.FAILEDRenderResult::FailedRenderResult.failedRenderResult.failedRenderResult.failed, release the session and attach
a new one.
// Draws the map if it changed since the last frame.static void demand_frame(void) { const mln_frame_demand demand = {.size = sizeof(mln_frame_demand)}; mln_render_session_demand_frame(session, &demand, NULL);}
static void drain_frame_results(void) { mln_render_frame_batch batch = MLN_HANDLE_NULL; mln_render_frame_batch_view view = { .size = sizeof(mln_render_frame_batch_view) }; if ( mln_render_session_drain_frame_results( session, &batch, &view, NULL ) != MLN_STATUS_OK || batch == MLN_HANDLE_NULL ) { return; // No results yet. }
bool failed = false; for (size_t i = 0; i < view.result_count; i++) { const mln_render_frame_result* result = MLN_STRIDED_AT( mln_render_frame_result, view.results, view.result_size, i ); if (result->disposition == MLN_RENDER_RESULT_FAILED) failed = true; } mln_render_frame_batch_release(batch);
if (failed) { replace_session(); // Dispose the session and attach a new one. }}// Draws the map if it changed since the last frame.void demandFrame() { session.demandFrame(const FrameDemand());}
void drainFrameResults() { final results = session.drainFrameResults(); if (results.isEmpty) { return; } if (results.any( (result) => result.disposition == RenderResult.failed, )) { replaceSession(); // Release the session and attach a new one. }}// Draws the map if it changed since the last frame.private static void DemandFrame() => session.DemandFrame(new FrameDemand());
private static void DrainFrameResults(){ var results = session.DrainFrameResults(); if (results.Length == 0) return; // No results yet.
if ( results.Any(result => result.Disposition == RenderResult.Failed ) ) // Release the session and attach a new one. ReplaceSession();}// Draws the map if it changed since the last frame.func demandFrame() error { return session.DemandFrame(maplibre.FrameDemand{})}
func drainFrameResults() error { results, err := session.DrainFrameResults() if err != nil { return err } if len(results) == 0 { return nil // No results yet. }
for _, result := range results { if result.Disposition == maplibre.RenderResultFailed { return replaceSession() // Release the session and attach a new one. } } return nil}// Draws the map if it changed since the last frame.bool demand_frame () { try { session.demand_frame ({}); } catch (GLib.Error error) { warning ("%s", error.message); } return GLib.Source.REMOVE;}
void drain_frame_results () throws GLib.Error { foreach (var result in session.drain_frame_results ()) { if (result.disposition == RenderResult.FAILED) { // Release the session and attach a new one. replace_session (); return; } }}// Draws the map if it changed since the last frame.private void demandFrame() { session.demandFrame(FrameDemand.of());}
private void drainFrameResults() { List<RenderFrameResult> results = session.drainFrameResults(); if (results.isEmpty()) return;
if (results.stream() .anyMatch( result -> result .getDisposition() .equals(RenderResult.FAILED))) { replaceSession(); // Release the session and attach a new one. }}// Draws the map if it changed since the last frame.private fun demandFrame() { session.demandFrame(FrameDemand())}
private fun drainFrameResults() { val results = session.drainFrameResults() if (results.isEmpty()) return
if (results.any { it.disposition == RenderResult.FAILED }) { replaceSession() // Release the session and attach a new one. }}# Draws the map if it changed since the last frame.def demand_frame() -> None: session.demand_frame(mln.FrameDemand())
def drain_frame_results() -> None: results = session.drain_frame_results() if not results: return if any( result.disposition == mln.RenderResult.FAILED for result in results ): replace_session() # Release the session and attach a new one.impl MapWindow { // Draws the map if it changed since the last frame. fn demand_frame(&mut self) -> Result<()> { self.session.demand_frame(&FrameDemand::default()) }
fn drain_frame_results(&mut self) -> Result<()> { let results = self.session.drain_frame_results()?; if results.is_empty() { return Ok(()); } if results .iter() .any(|result| result.disposition == RenderResult::Failed) { // Release the session and attach a new one. self.replace_session(); } Ok(()) }}/// Draws the map if it changed since the last frame.@MainActor private func demandFrame() throws { try session.demandFrame(FrameDemand())}
@MainActor private func drainFrameResults() throws { let results = try session.drainFrameResults() guard !results.isEmpty else { return // No results yet. }
if results.contains(where: { $0.disposition == .failed }) { replaceSession() // Release the session and attach a new one. }}// Draws the map if it changed since the last frame.function demandFrame(): void { session.demandFrame({});}
function drainFrameResults(): void { const results = session.drainFrameResults(); if (results.length === 0) { return; } if ( results.some( ({ disposition }) => disposition === RenderResult.failed, ) ) { replaceSession(); // Release the session and attach a new one. }}// Draws the map if it changed since the last frame.fn demandFrame() !void { try session.demandFrame(.{}, null);}
fn drainFrameResults() !void { var results = try session.drainFrameResults(allocator, null); defer results.deinit(); if (results.value.len == 0) return; // No results yet.
for (results.value) |result| { if (result.disposition == .failed) { // Dispose the session and attach a new one. return replaceSession(); } }}Resize the window
Section titled “Resize the window”When the window’s size changes, resize the session with
mln_render_session_resizeRenderSessionHandle.resizeRenderSessionHandle.ResizeAsyncRenderSessionHandle.Resizemaplibre_native_ffi_render_session_resize_asyncRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSessionHandle::resizeRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSession.resize. The session resizes the map for you.
static void on_window_resized(uint32_t width, uint32_t height) { // The window's new size in logical pixels. const mln_logical_size size = {.width = width, .height = height}; mln_render_session_resize(session, size, NULL, NULL);}void onWindowResized(int width, int height) { // The window's new size in logical pixels. session.discarding.resize( LogicalSize(width: width, height: height), );}private static void OnWindowResized(uint width, uint height){ // The window's new size in logical pixels. session.Discarding.Resize(new LogicalSize(width, height));}func onWindowResized(width, height uint32) error { // The window's new size in logical pixels. return session.Discarding().Resize(maplibre.LogicalSize{ Width: width, Height: height, })}void on_window_resized (uint32 width, uint32 height) { // The window's new size in logical pixels. session.resize_async.begin ({ width, height });}private void onWindowResized(int width, int height) { // The window's new size in logical pixels. session.getDiscarding().resize(LogicalSize.of(width, height));}private fun onWindowResized(width: UInt, height: UInt) { // The window's new size in logical pixels. session.discarding.resize( LogicalSize(width = width, height = height), )}def on_window_resized(width: int, height: int) -> None: # The window's new size in logical pixels. session.discarding.resize( mln.LogicalSize(width=width, height=height) )impl MapWindow { fn on_window_resized( &mut self, width: u32, height: u32, ) -> Result<()> { // The window's new size in logical pixels. let size = LogicalSize::new(width, height); self.session.discarding().resize(size) }}@MainActor private func onWindowResized( width: UInt32, height: UInt32) throws { // The window's new size in logical pixels. try session.discarding.resize( logicalSize: LogicalSize(width: width, height: height) )}function onWindowResized(width: number, height: number): void { // The window's new size in logical pixels. session.discarding.resize({ width, height });}fn onWindowResized(width: u32, height: u32) !void { // The window's new size in logical pixels. try session.discarding().resize(.{ .width = width, .height = height, }, null);}Handle a lost surface
Section titled “Handle a lost surface”Platforms can take the surface away, for example when your app goes to the background. Rather than releasing the session, release its target, so the map keeps its tiles and renderer state for when the surface comes back.
Release the target with mln_render_session_release_targetRenderSessionHandle.releaseTargetRenderSessionHandle.ReleaseTargetAsyncRenderSessionHandle.ReleaseTargetmaplibre_native_ffi_render_session_release_target_asyncRenderSessionHandle.releaseTargetRenderSessionHandle.releaseTargetRenderSessionHandle.release_targetRenderSessionHandle::release_targetRenderSessionHandle.releaseTargetRenderSessionHandle.releaseTargetRenderSession.releaseTarget before you
destroy the surface. On Android and OpenHarmony, the platform destroys the
window, so release the target when it does. You don’t need to wait for the
release to complete, because the session keeps its own reference to the window.
The session doesn’t call the redraw wake while it has no target. When the
platform gives you a surface again, set it and its size as the session’s target,
and the redraw wake asks for a frame again.
mln_render_session_release_target(session, &done, NULL);
const mln_vulkan_surface_replacement replacement = { .size = sizeof(mln_vulkan_surface_replacement), .logical_size = {.width = 800, .height = 600}, .surface = new_surface,};mln_render_session_set_vulkan_surface_target( session, &replacement, &done, NULL);await session.releaseTarget();
await session.setVulkanSurfaceTarget( VulkanSurfaceReplacement( logicalSize: const LogicalSize(width: 800, height: 600), surface: newSurface, ),);await session.ReleaseTargetAsync();
await session.SetVulkanSurfaceTargetAsync( new VulkanSurfaceReplacement { LogicalSize = new LogicalSize(800, 600), Surface = newSurface, });released, err := session.ReleaseTarget()if err != nil { return err}if _, err := released.Await(ctx); err != nil { return err}
replacement := maplibre.VulkanSurfaceReplacement{ LogicalSize: maplibre.LogicalSize{Width: 800, Height: 600}, Surface: newSurface,}restored, err := session.SetVulkanSurfaceTarget(replacement)if err != nil { return err}if _, err := restored.Await(ctx); err != nil { return err}yield session.release_target_async ();
yield session.set_vulkan_surface_target_async ( { logical_size: { width: 800, height: 600 }, surface: new_surface });session.releaseTarget().join();
MaplibreNative.setVulkanSurfaceTarget( session, VulkanSurfaceReplacement.of( LogicalSize.of(800, 600), newSurface)) .join();session.releaseTarget().await()
session .setVulkanSurfaceTarget( VulkanSurfaceReplacement( logicalSize = LogicalSize(width = 800u, height = 600u), surface = newSurface, ), ) .await()session.release_target().result()
session.set_vulkan_surface_target( mln.VulkanSurfaceReplacement( logical_size=mln.LogicalSize(width=800, height=600), surface=new_surface, )).result()window.session.release_target()?.await?;
let replacement = VulkanSurfaceReplacement::new( LogicalSize::new(800, 600), new_surface,);// SAFETY: the surface outlives the session.unsafe { window.session.set_vulkan_surface_target(&replacement)?}.await?;try await session.releaseTarget()
try await session.setVulkanSurfaceTarget( replacement: VulkanSurfaceReplacement( logicalSize: LogicalSize(width: 800, height: 600), surface: newSurface ))await session.releaseTarget();
await session.setVulkanSurfaceTarget({ logicalSize: { width: 800, height: 600 }, surface: newSurface,});try (try session.releaseTarget(null)).await(null);
const replaced = try session.setVulkanSurfaceTarget(.{ .logical_size = .{ .width = 800, .height = 600 }, .surface = new_surface,}, null);try replaced.await(null);Service driver work
Section titled “Service driver work”With the graphics-thread driver, the session does its graphics work only when
you service it with mln_render_session_service_driver_workmln_render_session_service_driver_workRenderSessionHandle.ServiceDriverWorkRenderSessionHandle.ServiceDriverWorkmaplibre_native_ffi_render_session_service_driver_workRenderSessionHandle.serviceDriverWorkRenderSessionHandle.serviceDriverWorkRenderSessionHandle.service_driver_workRenderSessionHandle::service_driver_workRenderSessionHandle.serviceDriverWorkRenderSessionHandle.serviceDriverWorkRenderSession.serviceDriverWork on your
graphics thread, with the target’s context current.
The mln_render_session_attach_options.driver_work_wakeRenderSessionAttachOptions.driverWorkWakeRenderSessionAttachOptions.DriverWorkWakeRenderSessionAttachOptions.DriverWorkWakeMaplibreNativeFfiRenderSessionAttachOptions:driver-work-wakeRenderSessionAttachOptions.driverWorkWakeRenderSessionAttachOptions.driverWorkWakeRenderSessionAttachOptions.driver_work_wakeRenderSessionAttachOptions::driver_work_wakeRenderSessionAttachOptions.driverWorkWakeRenderSessionAttachOptions.driverWorkWakeRenderSessionAttachOptions.driver_work_wake tells you when
there’s work to do. Each time it fires, schedule a service on your graphics
thread, even while the frame loop is stopped. After you release the session,
keep servicing it until the release completes, so it can free its graphics
objects on your context. If you release the map first, the session keeps those
objects until the process exits.
// Your graphics context is current on this thread.static void service_session(void) { mln_render_session_service_driver_work(session, NULL);}
// Runs on a library thread.static void on_driver_work_wake(void* user_data) { (void)user_data; run_on_graphics_thread(service_session);}
const mln_render_session_attach_options graphics_thread_options = { .size = sizeof(mln_render_session_attach_options), .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, .redraw_wake = {.callback = on_redraw_wake}, .frame_wake = {.callback = on_frame_wake}, .driver_work_wake = {.callback = on_driver_work_wake},};// An isolate can't service the session, because it can resume on// another thread after any await. Your native wake schedules// mln_render_session_service_driver_work on your graphics thread,// with the session's id.final graphicsThreadOptions = RenderSessionAttachOptions( driver: RenderDriverKind.callerGraphicsThread, redrawWake: const Wake(callback: onRedrawWake), frameWake: const Wake(callback: onFrameWake), driverWorkWake: Wake.native(callback: onDriverWorkWake),);// Your graphics context is current on this thread.private static void ServiceSession() => session.ServiceDriverWork();
// Runs on a library thread.private static void OnDriverWorkWake() => RunOnGraphicsThread(ServiceSession);
var graphicsThreadOptions = new RenderSessionAttachOptions{ Driver = RenderDriverKind.CallerGraphicsThread, RedrawWake = new Wake(OnRedrawWake), FrameWake = new Wake(OnFrameWake), DriverWorkWake = new Wake(OnDriverWorkWake),};// Your graphics context is current on this thread.// Its goroutine holds runtime.LockOSThread.func serviceSession() error { return session.ServiceDriverWork()}
// Runs on a library thread.func onDriverWorkWake() { runOnGraphicsThread(serviceSession)}
graphicsThreadOptions := maplibre.RenderSessionAttachOptions{ Driver: maplibre.RenderDriverKindCallerGraphicsThread, RedrawWake: maplibre.Wake{Callback: onRedrawWake}, FrameWake: maplibre.Wake{Callback: onFrameWake}, DriverWorkWake: maplibre.Wake{Callback: onDriverWorkWake},}// Your graphics context is current on this thread.bool service_session () { try { session.service_driver_work (); } catch (GLib.Error error) { warning ("%s", error.message); } return GLib.Source.REMOVE;}
// Runs on the main context of the thread that attached the// session.void on_driver_work_wake () { run_on_graphics_thread (service_session);}
var graphics_thread_options = new RenderSessionAttachOptions () { driver = RenderDriverKind.CALLER_GRAPHICS_THREAD, redraw_wake = wake (on_redraw_wake), frame_wake = wake (on_frame_wake), driver_work_wake = wake (on_driver_work_wake),};// Your graphics context is current on this thread.private void serviceSession() { session.serviceDriverWork();}
// Runs on a library thread.private void onDriverWorkWake() { runOnGraphicsThread(this::serviceSession);}
RenderSessionAttachOptions graphicsThreadOptions = RenderSessionAttachOptions.of( RenderDriverKind.CALLER_GRAPHICS_THREAD) .withRedrawWake(new Wake(this::onRedrawWake)) .withFrameWake(new Wake(this::onFrameWake)) .withDriverWorkWake(new Wake(this::onDriverWorkWake));// Your graphics context is current on this thread.private fun serviceSession() { session.serviceDriverWork()}
// Runs on a library thread.private fun onDriverWorkWake() { runOnGraphicsThread(::serviceSession)}
val graphicsThreadOptions = RenderSessionAttachOptions( driver = RenderDriverKind.CALLER_GRAPHICS_THREAD, redrawWake = Wake(::onRedrawWake), frameWake = Wake(::onFrameWake), driverWorkWake = Wake(::onDriverWorkWake), )# Your graphics context is current on this thread.def service_session() -> None: session.service_driver_work()
# Runs on the binding's dispatcher thread.def on_driver_work_wake() -> None: run_on_graphics_thread(service_session)
graphics_thread_options = mln.RenderSessionAttachOptions( driver=mln.RenderDriverKind.CALLER_GRAPHICS_THREAD, redraw_wake=mln.Wake(on_redraw_wake), frame_wake=mln.Wake(on_frame_wake), driver_work_wake=mln.Wake(on_driver_work_wake),)impl MapWindow { // Your graphics context is current on this thread. fn service_session(&mut self) -> Result<()> { self.session.service_driver_work() }}
// Runs on a library thread.fn on_driver_work_wake() { run_on_graphics_thread(MapWindow::service_session);}
let graphics_thread_options = RenderSessionAttachOptions { redraw_wake: Wake::new(on_redraw_wake), frame_wake: Wake::new(on_frame_wake), driver_work_wake: Wake::new(on_driver_work_wake), ..RenderSessionAttachOptions::new( RenderDriverKind::CallerGraphicsThread, )};/// Runs on a library thread.private func onDriverWorkWake() { // Read the session on the main actor, which owns it. runOnEventLoop { let current: RenderSessionHandle = session runOnGraphicsThread { // Your graphics context is current on this thread. try current.serviceDriverWork() } }}
let graphicsThreadOptions = RenderSessionAttachOptions( driver: .callerGraphicsThread, frameWake: Wake(callback: onFrameWake), redrawWake: Wake(callback: onRedrawWake), driverWorkWake: Wake(callback: onDriverWorkWake))// The binding calls the wake on the JS thread. Your graphics context// is current there.function onDriverWorkWake(): void { session.serviceDriverWork();}
const graphicsThreadOptions: RenderSessionAttachOptions = { driver: RenderDriverKind.callerGraphicsThread, redrawWake: { callback: onRedrawWake }, frameWake: { callback: onFrameWake }, driverWorkWake: { callback: onDriverWorkWake },};// Your graphics context is current on this thread.fn serviceSession() !void { try session.serviceDriverWork(null);}
// Runs on a library thread.fn onDriverWorkWake(_: ?*anyopaque) anyerror!void { runOnGraphicsThread(serviceSession);}
const graphics_thread_options = maplibre.RenderSessionAttachOptions{ .driver = .caller_graphics_thread, .redraw_wake = .{ .callback = onRedrawWake }, .frame_wake = .{ .callback = onFrameWake }, .driver_work_wake = .{ .callback = onDriverWorkWake }, };