Skip to content

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.

First, create the window with your platform or windowing library, and get the surface handle for your backend:

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.

main.c
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
);
main.dart
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;
Program.cs
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;
main.go
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 = attached
if _, err := ready.Await(ctx); err != nil {
return err
}
main.vala
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 ();
Main.java
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();
Main.kt
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()
main.py
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()
main.rs
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 };
main.swift
@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.ready
main.ts
let 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;
main.zig
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);

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.

The 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.

main.c
// 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);
}
main.dart
// The binding calls both wakes in this isolate,
// so the frame wake can drain right away.
void onRedrawWake() {
atNextRefresh(demandFrame);
}
void onFrameWake() {
drainFrameResults();
}
Program.cs
// Both run on a library thread.
private static void OnRedrawWake() =>
RunAtNextRefresh(DemandFrame);
private static void OnFrameWake() =>
RunOnEventLoop(DrainFrameResults);
main.go
// Both run on a library thread.
func onRedrawWake() {
runAtNextRefresh(demandFrame)
}
func onFrameWake() {
runOnEventLoop(drainFrameResults)
}
main.vala
// 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);
}
}
Main.java
// Both run on a library thread.
private void onRedrawWake() {
runAtNextRefresh(this::demandFrame);
}
private void onFrameWake() {
runOnEventLoop(this::drainFrameResults);
}
Main.kt
// Both run on a library thread.
private fun onRedrawWake() {
runAtNextRefresh(::demandFrame)
}
private fun onFrameWake() {
runOnEventLoop(::drainFrameResults)
}
main.py
# 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)
main.rs
// 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);
}
main.swift
/// Both run on a library thread.
private func onRedrawWake() {
atNextRefresh(demandFrame)
}
private func onFrameWake() {
runOnEventLoop(drainFrameResults)
}
main.ts
// 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();
}
main.zig
// 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.

main.c
// 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.
}
}
main.dart
// 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.
}
}
Program.cs
// 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();
}
main.go
// 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
}
main.vala
// 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;
}
}
}
Main.java
// 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.
}
}
Main.kt
// 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.
}
}
main.py
# 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.
main.rs
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(())
}
}
main.swift
/// 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.
}
}
main.ts
// 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.
}
}
main.zig
// 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();
}
}
}

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.

main.c
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);
}
main.dart
void onWindowResized(int width, int height) {
// The window's new size in logical pixels.
session.discarding.resize(
LogicalSize(width: width, height: height),
);
}
Program.cs
private static void OnWindowResized(uint width, uint height)
{
// The window's new size in logical pixels.
session.Discarding.Resize(new LogicalSize(width, height));
}
main.go
func onWindowResized(width, height uint32) error {
// The window's new size in logical pixels.
return session.Discarding().Resize(maplibre.LogicalSize{
Width: width, Height: height,
})
}
main.vala
void on_window_resized (uint32 width, uint32 height) {
// The window's new size in logical pixels.
session.resize_async.begin ({ width, height });
}
Main.java
private void onWindowResized(int width, int height) {
// The window's new size in logical pixels.
session.getDiscarding().resize(LogicalSize.of(width, height));
}
Main.kt
private fun onWindowResized(width: UInt, height: UInt) {
// The window's new size in logical pixels.
session.discarding.resize(
LogicalSize(width = width, height = height),
)
}
main.py
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)
)
main.rs
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)
}
}
main.swift
@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)
)
}
main.ts
function onWindowResized(width: number, height: number): void {
// The window's new size in logical pixels.
session.discarding.resize({ width, height });
}
main.zig
fn onWindowResized(width: u32, height: u32) !void {
// The window's new size in logical pixels.
try session.discarding().resize(.{
.width = width,
.height = height,
}, null);
}

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.

main.c
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
);
main.dart
await session.releaseTarget();
await session.setVulkanSurfaceTarget(
VulkanSurfaceReplacement(
logicalSize: const LogicalSize(width: 800, height: 600),
surface: newSurface,
),
);
Program.cs
await session.ReleaseTargetAsync();
await session.SetVulkanSurfaceTargetAsync(
new VulkanSurfaceReplacement
{
LogicalSize = new LogicalSize(800, 600),
Surface = newSurface,
}
);
main.go
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
}
main.vala
yield session.release_target_async ();
yield session.set_vulkan_surface_target_async (
{ logical_size: { width: 800, height: 600 }, surface: new_surface }
);
Main.java
session.releaseTarget().join();
MaplibreNative.setVulkanSurfaceTarget(
session,
VulkanSurfaceReplacement.of(
LogicalSize.of(800, 600), newSurface))
.join();
Main.kt
session.releaseTarget().await()
session
.setVulkanSurfaceTarget(
VulkanSurfaceReplacement(
logicalSize = LogicalSize(width = 800u, height = 600u),
surface = newSurface,
),
)
.await()
main.py
session.release_target().result()
session.set_vulkan_surface_target(
mln.VulkanSurfaceReplacement(
logical_size=mln.LogicalSize(width=800, height=600),
surface=new_surface,
)
).result()
main.rs
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?;
main.swift
try await session.releaseTarget()
try await session.setVulkanSurfaceTarget(
replacement: VulkanSurfaceReplacement(
logicalSize: LogicalSize(width: 800, height: 600),
surface: newSurface
)
)
main.ts
await session.releaseTarget();
await session.setVulkanSurfaceTarget({
logicalSize: { width: 800, height: 600 },
surface: newSurface,
});
main.zig
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);

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.

main.c
// 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},
};
main.dart
// 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),
);
Program.cs
// 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),
};
main.go
// 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},
}
main.vala
// 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),
};
Main.java
// 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));
Main.kt
// 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),
)
main.py
# 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),
)
main.rs
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,
)
};
main.swift
/// 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)
)
main.ts
// 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 },
};
main.zig
// 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 },
};